Agent 很擅长写测试,但自动生成的测试不一定真的有价值。它可能只测试实现细节、复制错误逻辑、遗漏边界条件,甚至写出“永远通过”的空测试。本文设计一套测试生成实验和质量门禁,判断 Agent 写出的测试是否真的增强质量。

Lab 004:Agent 自动写测试,是增强质量还是制造幻觉?

Agent 很擅长写测试,但自动生成的测试不一定真的有价值。它可能只测试实现细节、复制错误逻辑、遗漏边界条件,甚至写出“永远通过”的空测试。本文设计一套测试生成实验和质量门禁,判断 Agent 写出的测试是否真的增强质量。

这套方法可以沉淀成团队的 Prompt 模板和测试工作流,并接入 CI/CD 作为回归质量门禁。

Agent 自动写测试质量门禁图
Agent 自动写测试质量门禁图

一、实验目标

我们要区分两种测试:

类型 特征
有效测试 能捕获真实缺陷,覆盖边界条件,失败时给出清晰信号
幻觉测试 只验证 mock、复制实现、没有断言或断言无意义

二、测试生成流程

mermaid
flowchart TD
  A["输入规格和源码"] --> B["Agent 生成测试计划"]
  B --> C["人工或规则检查测试计划"]
  C --> D["Agent 写测试代码"]
  D --> E["运行测试"]
  E --> F{"是否能失败"}
  F -->|否| G["怀疑空测试或弱断言"]
  F -->|是| H["修复实现或确认缺陷"]
  H --> I["变异检查"]
  I --> J["纳入回归测试"]

三、给 Agent 的输入材料

测试生成不能只给源码,还要给规格和历史缺陷。

yaml
test_generation_input:
  feature_name: "订单优惠券计算"
  source_files:
    - "app/order/discount.py"
  spec:
    - "百分比优惠券最多抵扣订单金额的 50%"
    - "固定金额优惠券不能让订单金额小于 0"
    - "过期优惠券必须被拒绝"
  known_bugs:
    - "曾经出现 fixed coupon 导致 total=-1 的问题"
  test_framework: "pytest"
  constraints:
    - "不要 mock 被测函数本身"
    - "必须包含边界值"
    - "每个测试必须有明确断言"

四、测试计划示例

让 Agent 先输出计划,而不是直接写文件。

md
## 测试计划

1. 百分比优惠券正常抵扣
   - 输入:total=100, percent=20
   - 期望:final_total=80

2. 百分比优惠券超过 50% 上限
   - 输入:total=100, percent=80
   - 期望:final_total=50

3. 固定金额优惠券超过订单金额
   - 输入:total=30, fixed=50
   - 期望:final_total=0

4. 过期优惠券
   - 输入:expires_at < now
   - 期望:抛出 CouponExpired

计划阶段要检查:

检查项 说明
是否覆盖正常路径 至少一个成功案例
是否覆盖边界值 上限、下限、空值、重复、过期
是否覆盖历史缺陷 已知 Bug 必须变成回归测试
是否有明确断言 不能只调用函数不检查结果

五、生成的测试代码

python
from datetime import datetime, timedelta, timezone

import pytest

from app.order.discount import Coupon, CouponExpired, apply_coupon


def test_percent_coupon_applies_discount():
    coupon = Coupon(kind="percent", value=20, expires_at=datetime.now(timezone.utc) + timedelta(days=1))

    assert apply_coupon(total=100, coupon=coupon) == 80


def test_percent_coupon_is_capped_at_half_total():
    coupon = Coupon(kind="percent", value=80, expires_at=datetime.now(timezone.utc) + timedelta(days=1))

    assert apply_coupon(total=100, coupon=coupon) == 50


def test_fixed_coupon_never_makes_total_negative():
    coupon = Coupon(kind="fixed", value=50, expires_at=datetime.now(timezone.utc) + timedelta(days=1))

    assert apply_coupon(total=30, coupon=coupon) == 0


def test_expired_coupon_is_rejected():
    coupon = Coupon(kind="fixed", value=10, expires_at=datetime.now(timezone.utc) - timedelta(days=1))

    with pytest.raises(CouponExpired):
        apply_coupon(total=100, coupon=coupon)

六、质量门禁

6.1 静态检查

python
import ast
from pathlib import Path


def test_generated_tests_have_assertions():
    for path in Path("tests/generated").glob("test_*.py"):
        tree = ast.parse(path.read_text())
        test_funcs = [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef) and n.name.startswith("test_")]
        assert test_funcs, f"{path} has no test functions"

        for func in test_funcs:
            has_assert = any(isinstance(n, ast.Assert) for n in ast.walk(func))
            has_pytest_raises = any(
                isinstance(n, ast.With)
                and any("pytest.raises" in ast.unparse(item.context_expr) for item in n.items)
                for n in ast.walk(func)
            )
            assert has_assert or has_pytest_raises, f"{func.name} has no assertion"

6.2 变异检查

最简单的变异检查是故意改坏实现,确认测试会失败:

bash
cp app/order/discount.py /tmp/discount.py.bak
python scripts/mutate_discount_cap.py

if pytest tests/generated/test_discount.py -q; then
  echo "generated tests did not catch mutation"
  cp /tmp/discount.py.bak app/order/discount.py
  exit 1
fi

cp /tmp/discount.py.bak app/order/discount.py

七、参数说明

参数 说明
source_files 被测源码文件,避免 Agent 全仓库乱扫
spec 行为规格,测试应验证规格而非实现细节
known_bugs 历史缺陷,必须转成回归测试
test_framework 指定 pytest、Vitest、Jest、Playwright 等
constraints 测试生成硬约束,例如不能 mock 被测函数

八、结果记录模板

yaml
test_generation_result:
  generated_files:
    - "tests/generated/test_discount.py"
  test_count: 4
  assertion_count: 4
  covers_known_bugs: true
  includes_boundary_cases: true
  initial_run:
    passed: true
    command: "pytest tests/generated/test_discount.py -q"
  mutation_check:
    passed: true
    killed_mutations: 3
    survived_mutations: 0
  reviewer_notes:
    - "测试验证业务规格,没有 mock 被测函数"

九、常见坏味道

  • 测试只断言返回值不为 None
  • 大量 mock 掉真正应该验证的逻辑
  • 测试名称很长但没有边界条件
  • 复制实现代码计算期望值
  • 只测 happy path,不测异常和历史缺陷
  • E2E 测试没有等待真实状态,只检查页面存在

总结

Agent 自动写测试可以显著提高覆盖率,但前提是给它规格、历史缺陷和明确约束。真正可靠的流程不是“让 Agent 随便补测试”,而是测试计划先行、断言检查、变异检查和人工审查一起构成质量门禁。