Lab 004:Agent 自动写测试,是增强质量还是制造幻觉?
Agent 很擅长写测试,但自动生成的测试不一定真的有价值。它可能只测试实现细节、复制错误逻辑、遗漏边界条件,甚至写出“永远通过”的空测试。本文设计一套测试生成实验和质量门禁,判断 Agent 写出的测试是否真的增强质量。
这套方法可以沉淀成团队的 Prompt 模板和测试工作流,并接入 CI/CD 作为回归质量门禁。
一、实验目标
我们要区分两种测试:
| 类型 | 特征 |
|---|---|
| 有效测试 | 能捕获真实缺陷,覆盖边界条件,失败时给出清晰信号 |
| 幻觉测试 | 只验证 mock、复制实现、没有断言或断言无意义 |
二、测试生成流程
flowchart TD
A["输入规格和源码"] --> B["Agent 生成测试计划"]
B --> C["人工或规则检查测试计划"]
C --> D["Agent 写测试代码"]
D --> E["运行测试"]
E --> F{"是否能失败"}
F -->|否| G["怀疑空测试或弱断言"]
F -->|是| H["修复实现或确认缺陷"]
H --> I["变异检查"]
I --> J["纳入回归测试"]三、给 Agent 的输入材料
测试生成不能只给源码,还要给规格和历史缺陷。
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 先输出计划,而不是直接写文件。
## 测试计划
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 必须变成回归测试 |
| 是否有明确断言 | 不能只调用函数不检查结果 |
五、生成的测试代码
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 静态检查
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 变异检查
最简单的变异检查是故意改坏实现,确认测试会失败:
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 被测函数 |
八、结果记录模板
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 随便补测试”,而是测试计划先行、断言检查、变异检查和人工审查一起构成质量门禁。