跳转到内容

四层测试体系架构

学习目标

  • 理解四层测试的设计哲学与各自职责
  • 掌握 _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魔数 %PDF-
DOCX/XLSX/PPTXZIP 结构(OOXML)
PNG/JPG魔数 + 像素分析
MP4ffmpeg 探测帧
音频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.md
## 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
GIMP1076443
Blender20815058
Inkscape20214854
LibreOffice1588969
s&box24415787

问题与规避

问题规避策略
真实后端测试在 CI 中无法运行CI 环境预装所有后端软件;不跳过,必须通过
子进程测试慢分层执行:CI 可以先跑单元,后跑 E2E
输出验证误报多重验证:魔数 + 结构 + 内容
_resolve_cli 回退到 python -mCLI_ANYTHING_FORCE_INSTALLED=1 在 CI 中强制使用已安装命令

设计取舍

测试计划先于测试编写

选择:Phase 4 写测试计划,Phase 5 按计划编写。

原因:防止”写完代码后随便补测试”导致的覆盖不全。先定义覆盖范围再实现,确保系统性。

不信任退出码

选择:验证输出内容,不仅检查 returncode == 0

原因:后端可能成功退出但产出无效文件。魔数验证是最低成本的可靠检查。

参考来源

  • 源码验证:HARNESS.md Testing Strategy 章节
  • 源码验证:gimp/agent-harness/tests/test_core.py + test_full_e2e.py
  • 测试统计:README 中的 Test Results 表格