Loading

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。最好的策略是金字塔形的——大量的快速单元测试在底层托底,少量的真实后端测试在最顶层守门。

在实际开发中,这个顺序(分析→设计→实现→测试规划→测试编写→文档→发布)保证了测试是在对功能有清晰预期的前提下写的,而不是写完了再想"我应该测点什么"。

posted @ 2026-03-23 13:04  饭勺oO  阅读(10)  评论(0)    收藏  举报