1,508 个测试,100% 通过:CLI 的四层测试策略
为什么测试策略值得单独说
很多 CLI 生成工具的做法是:生成代码,然后简单跑一遍 --help 看看有没有语法错误。这对 demo 来说够了,但如果这个 CLI 要真正用于生产——Agent 每天调用几十次——这种测试水平是不够的。
CLI-Anything 的测试策略分四层,层层递进。最底层的单元测试验证单个函数的正确性,最顶层的子进程测试验证整个安装包的行为。这套体系是人工写生产级 CLI 时常用的测试方法,CLI-Anything 把它自动化了。
第一层:单元测试
单元测试的目标是:不依赖任何外部软件,快速验证核心逻辑的正确性。
以 Blender CLI 为例,blender/core/scene.py 里的 SceneManager 类会被单元测试覆盖:
# test_core_scene.py
import pytest
import json
from cli_anything.blender.core.scene import SceneManager
def test_scene_new_creates_valid_json(tmp_path):
manager = SceneManager()
result = manager.new(name="TestScene", width=1280, height=720)
assert result.path.exists()
with open(result.path) as f:
data = json.load(f)
assert data["name"] == "TestScene"
assert data["scene"]["size"] == {"x": 1280, "y": 720}
def test_scene_add_object_updates_state(tmp_path):
manager = SceneManager()
manager.new(name="TestScene")
manager.add_object("Cube", obj_type="cube")
data = json.loads(manager.state_json)
assert len(data["objects"]) == 1
assert data["objects"][0]["type"] == "cube"
单元测试用合成数据,不需要 Blender 真的在运行,所以跑得很快。Blender CLI 的 150 个单元测试大概几秒钟就能跑完。
第二层:端到端测试(原生)
第二层测试验证生成的中间文件格式是否正确。这里的"原生"指的是不调用真实软件,只验证文件格式。
以 LibreOffice CLI 为例,生成的是 ODF(Open Document Format)ZIP 包。测试会解压它,检查内部 XML 结构:
# test_e2e_writer.py
import zipfile
import pytest
from cli_anything.libreoffice.core.writer import WriterDocument
def test_writer_generates_valid_odf(tmp_path):
doc = WriterDocument()
doc.add_heading("Q1 Report", level=1)
doc.add_table(rows=3, cols=3)
output_path = tmp_path / "report.odt"
doc.save(output_path)
# 验证是合法的 ZIP(ODF 本质是 ZIP)
assert zipfile.is_zipfile(output_path)
# 验证包含必要的 ODF 组件
with zipfile.ZipFile(output_path) as z:
names = z.namelist()
assert "mimetype" in names
assert "content.xml" in names
assert "styles.xml" in names
# 验证 content.xml 非空且格式合法
with zipfile.ZipFile(output_path) as z:
content = z.read("content.xml").decode("utf-8")
assert "<text:h" in content # ODF 标题标签
assert "<table:table" in content # ODF 表格标签
这类测试的意义在于:在调用真实 LibreOffice 之前,先确保生成的中间文件本身格式合法。如果 ODF 格式有问题,测试会直接失败,而不需要等 LibreOffice 报错才知道。
Inkscape CLI 的 SVG 格式验证、Shotcut CLI 的 MLT XML 格式验证都属于这一层。
第三层:端到端测试(真实后端)
这是最严格的一层,也是和其他 CLI 生成工具拉开差距的一层。
必须真正调用目标软件,并验证输出。
Blender 的这部分测试会实际运行 Blender 的无头模式:
# test_e2e_blender.py
import subprocess
import pytest
from pathlib import Path
def test_blender_render_produces_valid_png(tmp_path):
# 先生成 .blend 文件
scene = generate_blender_scene()
blend_path = tmp_path / "scene.blend"
scene.save(blend_path)
# 真正运行 Blender 渲染
result = subprocess.run([
"blender",
"--background",
str(blend_path),
"--python-expr",
"import bpy; bpy.ops.render.render(write_still=True, "
f"filepath='{tmp_path / \"output.png\"}')"
], capture_output=True, text=True, timeout=120)
# 验证返回码
assert result.returncode == 0, f"Blender failed: {result.stderr}"
# 验证输出文件存在
output_png = tmp_path / "output.png"
assert output_png.exists()
# 验证是真实的 PNG 文件(魔数检测)
with open(output_png, "rb") as f:
magic = f.read(8)
assert magic[:8] == b"\\x89PNG\\r\\n\\x1a\\n"
关键设计决策:如果 Blender 没有安装,这一层的测试会直接失败,而不是跳过。
def test_blender_render_produces_valid_png(tmp_path):
if shutil.which("blender") is None:
pytest.skip("Blender not installed")
这种"跳过而非失败"的策略在玩具级工具里很常见——假装一切正常,给用户一个虚假的通过率。CLI-Anything 的选择是:宁可测试失败,也不给虚假通过。
真实后端测试的覆盖范围:
- Blender:真正运行
--background渲染模式,验证 PNG 输出 - LibreOffice:真正调用 headless 模式渲染 PDF,验证 PDF 魔数
- GIMP:运行 Script-Fu 脚本,验证处理后的图像文件
- Audacity:通过 sox 处理音频,验证 WAV 头信息
第四层:CLI 子进程测试
前三层测试在 Python 代码内部运行。第四层测试是从外部进程调用安装后的命令,验证整个安装包没有错误。
# test_cli_subprocess.py
import subprocess
import json
import pytest
def test_cli_installed_and_responds_to_help():
result = subprocess.run(
["cli-anything-gimp", "--help"],
capture_output=True,
text=True
)
assert result.returncode == 0
assert "Usage:" in result.stdout or "Usage:" in result.stderr
def test_cli_json_mode_produces_valid_json(tmp_path):
result = subprocess.run(
["cli-anything-gimp", "--json", "project", "new",
"--width", "1280", "--height", "720",
"-o", str(tmp_path / "test.json")],
capture_output=True,
text=True
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert "width" in data
assert data["width"] == 1280
这一层测试的意义是:确保 pip install -e . 生成的 entry_point 脚本真的能用,确保 --json 输出的 JSON 真的合法。
测试结果的横向对比
| 软件 | 单元测试 | E2E 原生 | E2E 真实后端 | CLI 子进程 | 合计 |
|---|---|---|---|---|---|
| GIMP | 64 | 30 | 13 | 43 | 107 |
| Blender | 150 | 45 | 13 | 58 | 208 |
| Inkscape | 148 | 40 | 14 | 54 | 202 |
| Audacity | 107 | 45 | 9 | 54 | 161 |
| LibreOffice | 89 | 50 | 19 | 69 | 158 |
| OBS Studio | 116 | 35 | 2 | 37 | 153 |
| Kdenlive | 111 | 40 | 14 | 44 | 155 |
| Shotcut | 110 | 40 | 14 | 44 | 154 |
| Zoom | 22 | 22 | 0 | 0 | 22 |
| Draw.io | 116 | 100 | 16 | 22 | 138 |
| AnyGen | 40 | 30 | 10 | 10 | 50 |
| 合计 | 1,073 | 477 | 134 | 435 | 1,508 |
Zoom 因为用的是 REST API,真实后端测试就是测 HTTP 请求,所以没有单独分出"真实后端"这一层。OBS Studio 的真实后端测试只有 2 个,是因为 obs-websocket 需要服务在运行,测试环境限制比较严。
这个测试策略教会我们什么
CLI-Anything 的四层测试策略其实是在说一件事:测试的深度和真实性成正比,和速度成反比。
单元测试最快,但只能验证逻辑;真实后端测试最慢,但才能验证功能真的 work。最好的策略是金字塔形的——大量的快速单元测试在底层托底,少量的真实后端测试在最顶层守门。
在实际开发中,这个顺序(分析→设计→实现→测试规划→测试编写→文档→发布)保证了测试是在对功能有清晰预期的前提下写的,而不是写完了再想"我应该测点什么"。

浙公网安备 33010602011771号