四层测试体系架构
学习目标
- 理解四层测试的设计哲学与各自职责
- 掌握
_resolve_cli()辅助函数的设计 - 应用”无优雅降级”的测试原则
- 了解 TEST.md 双用途文档模式
前置知识
本章涉及 Agent-Native 软件的通用测试策略,建议先阅读:
下文直接聚焦 CLI-Anything 的四层测试实现。
项目实践
四层测试全景
L1:单元测试(test_core.py)
测试每个核心函数在隔离环境下的行为:
# 伪代码:GIMP 单元测试def test_create_project(): proj = create_project("Test", "image", 1920, 1080) assert proj["name"] == "Test" assert proj["width"] == 1920
def test_add_layer(): proj = create_project("Test", "image", 800, 600) result = add_layer(proj, "Background", "solid", "#1a1a2e") assert len(proj["layers"]) == 1 assert proj["layers"][0]["name"] == "Background"特征:
- 合成数据,无外部依赖(不需要 GIMP 安装)
- 快速、确定性、适合 CI
- GIMP Harness 包含 64 个单元测试
L2:E2E 原生测试
验证 CLI 生成的中间文件结构正确:
# 伪代码:LibreOffice E2E 原生测试def test_odf_structure(tmp_path): proj = create_document("writer", "Report") add_heading(proj, "Title", level=1) odf_path = save_odf(tmp_path, proj)
# 验证 ODF 是合法的 ZIP assert zipfile.is_zipfile(odf_path)
# 验证内部 XML 结构 with zipfile.ZipFile(odf_path) as z: assert "content.xml" in z.namelist() content = z.read("content.xml") assert b"Title" in content特征:
- 不依赖真实软件,只验证中间文件合法性
- ODF ZIP 结构、MLT XML 合法性、SVG 规范性
L3:E2E 真实后端测试
必须调用真实软件,验证最终输出:
# 伪代码:LibreOffice 真实后端测试def test_writer_to_pdf(tmp_path): proj = create_document("writer", "Report") add_heading(proj, "Quarterly Report", level=1)
pdf_path = str(tmp_path / "report.pdf") result = export(proj, pdf_path, preset="pdf", overwrite=True)
# 验证真实输出 assert os.path.exists(result["output"]) assert result["file_size"] > 1000 # 不为空 with open(result["output"], "rb") as f: assert f.read(5) == b"%PDF-" # 验证 PDF 魔数 print(f"\n PDF: {result['output']} ({result['file_size']:,} bytes)")关键验证方法:
| 输出类型 | 验证方式 |
|---|---|
魔数 %PDF- | |
| DOCX/XLSX/PPTX | ZIP 结构(OOXML) |
| PNG/JPG | 魔数 + 像素分析 |
| MP4 | ffmpeg 探测帧 |
| 音频 | RMS 级别、频谱对比 |
L4:CLI 子进程测试
通过 _resolve_cli() 测试已安装的命令:
def _resolve_cli(name): """Resolve installed CLI command; falls back to python -m for dev.""" import shutil force = os.environ.get("CLI_ANYTHING_FORCE_INSTALLED", "").strip() == "1" path = shutil.which(name) if path: return [path] if force: raise RuntimeError(f"{name} not found in PATH.") module = name.replace("cli-anything-", "cli_anything.") + "..." return [sys.executable, "-m", module]
class TestCLISubprocessE2E: CLI_BASE = _resolve_cli("cli-anything-libreoffice")
def _run(self, args, check=True): return subprocess.run( self.CLI_BASE + args, capture_output=True, text=True, check=check, )
def test_full_writer_pdf_workflow(self, tmp_path): proj_path = str(tmp_path / "test.json") pdf_path = str(tmp_path / "output.pdf") self._run(["document", "new", "-o", proj_path, "--type", "writer"]) self._run(["--project", proj_path, "writer", "add-heading", "-t", "Title"]) self._run(["--project", proj_path, "export", "render", pdf_path, "-p", "pdf", "--overwrite"]) assert os.path.exists(pdf_path) with open(pdf_path, "rb") as f: assert f.read(5) == b"%PDF-"关键规则:
- 使用
_resolve_cli()而非硬编码路径 - 不设置
cwd——已安装命令必须从任何目录工作 CLI_ANYTHING_FORCE_INSTALLED=1在 CI 中确保测试真实安装
无优雅降级原则
HARNESS.md 明确要求:
The real software MUST be installed. Tests must NOT skip or fake results when the software is missing.
这意味着:
- 测试套件中没有
pytest.mark.skipif(not has_gimp)之类的条件跳过 - 缺少真实软件时测试失败而非跳过
- 这确保了 CLI 的可靠性——不依赖优雅降级
TEST.md 双用途文档
TEST.md 在 Phase 4(测试计划)和 Phase 6(测试结果)中扮演双重角色:
## Test Inventory Plan- test_core.py: 64 unit tests planned- test_full_e2e.py: 43 E2E tests planned
## Unit Test Plan### project.py- Functions: create_project, open_project, save_project- Edge cases: invalid format, missing fields- Expected: 12 tests
## E2E Test Plan### Writer to PDF- Simulates: Report generation- Operations: create → add heading → add table → export PDF- Verified: PDF magic bytes, file size > 1000
---## Test Results (Appended in Phase 6)================================ Test Summary ================================ gimp 107 passed ✅ (64 unit + 43 e2e)
测试统计
CLI-Anything 的 2330 项测试 100% 通过:
| Harness | 总计 | 单元 | E2E |
|---|---|---|---|
| GIMP | 107 | 64 | 43 |
| Blender | 208 | 150 | 58 |
| Inkscape | 202 | 148 | 54 |
| LibreOffice | 158 | 89 | 69 |
| s&box | 244 | 157 | 87 |
问题与规避
| 问题 | 规避策略 |
|---|---|
| 真实后端测试在 CI 中无法运行 | CI 环境预装所有后端软件;不跳过,必须通过 |
| 子进程测试慢 | 分层执行:CI 可以先跑单元,后跑 E2E |
| 输出验证误报 | 多重验证:魔数 + 结构 + 内容 |
_resolve_cli 回退到 python -m | CLI_ANYTHING_FORCE_INSTALLED=1 在 CI 中强制使用已安装命令 |
设计取舍
测试计划先于测试编写
选择:Phase 4 写测试计划,Phase 5 按计划编写。
原因:防止”写完代码后随便补测试”导致的覆盖不全。先定义覆盖范围再实现,确保系统性。
不信任退出码
选择:验证输出内容,不仅检查 returncode == 0。
原因:后端可能成功退出但产出无效文件。魔数验证是最低成本的可靠检查。
参考来源
- 源码验证:
HARNESS.mdTesting Strategy 章节 - 源码验证:
gimp/agent-harness/tests/test_core.py+test_full_e2e.py - 测试统计:README 中的 Test Results 表格