AI 生成的测试往往只覆盖愉快路径,新增格式破坏时仍全绿;文章推荐用变异测试(mutation testing)验证测试质量,并指出免费 Token 额度使该方法足够便宜可日常化。
有位开发者最近让一个 coding agent 为一个解析 ISO 日期的函数编写测试套件。agent 返回了 14 个测试用例,全部通过,开发者看都没看就合并了。一周后,一位同事扩展了该函数以接受两种额外的时间戳格式,但套件仍然通过——尽管这两种新格式其实都是坏的。这些测试只是用略微不同的输入断言同一条 happy path,没有一个用例锁死该函数本来要处理的边界情况。
目前关于 AI coding agent 的讨论集中在它们生成的代码上,但它们编写的测试同样需要审视。agent 生成的绿色套件不等于有意义的套件,这个区别很重要,因为虚假的信心比没有测试更危险。Mutation testing(变异测试)提供了一种严格的方式来衡量两者之间的差异,而一个免费的、拥有慷慨 token 配额的服务端使得这种衡量足够廉价,可以对每个 agent 生成的套件运行。
Mutation testing 的工作原理是:在源代码中引入小的、刻意的 bug,然后检查测试套件是否能捕获它们。每个刻意的 bug 就是一个 mutant,而 mutation score 是套件杀死的 mutant 百分比。一个得分低于 50% 的套件基本是装饰性的,无论它包含多少个断言。这项技术早已成熟,但素有"运行慢"的名声——而这正是一款可支配的远程工作空间改变成本效益的地方。
这里描述的工作流使用 MonkeyCode,这是一个开源项目,目前提供免费模型访问和免费服务端选项。披露:本文是作为 MonkeyCode 产品推广的一部分撰写的。免费模型访问包含写作时 1000 万 token 的配额,足够为同一函数生成多个候选套件;免费服务端则提供了一个干净的环境,可以运行 mutation testing 而不占用本地机器。两者都是当前有效但非永久的,因此在围绕它们构建工作流之前请先核实。
审计遵循固定的顺序。agent 为特定函数编写测试套件,生成的文件被保存以供检查。然后将仓库克隆到免费服务端上的一个新工作空间中,再对 agent 的套件运行 mutation testing 工具。存活下来的 mutant 是最终输出,因为它们精确展示了 agent 的测试未能锁定的行为。
下面的脚本将审计过程自动化了。它接收一个仓库 URL、源文件、agent 生成的测试文件和 mutator 名称,然后克隆仓库、安装依赖、将测试文件放入正确位置,再运行 mutation 工具。
#!/usr/bin/env bash
set -euo pipefail
# mutant_audit.sh — mutation-test a suite written by a coding agent
# usage: ./mutant_audit.sh <repo-url> <source-file> <test-file> [mutator]
REPO_URL="${1:?repo url required}"
SOURCE_FILE="${2:?source file required}"
TEST_FILE="${3:?test file required}"
MUTATOR="${4:-stryker}"
BASE="$(mktemp -d /tmp/mutant_audit.XXXXXX)"
git clone --depth 1 "$REPO_URL" "$BASE/repo" >/dev/null 2>&1
cd "$BASE/repo"
# install dependencies quietly
if [ -f package.json ]; then
npm ci --silent
elif [ -f pyproject.toml ] || [ -f requirements.txt ]; then
pip install -e . -q
fi
# copy the agent-generated test file into the project
if [ -d tests ]; then
cp "$TEST_FILE" tests/
else
cp "$TEST_FILE" .
fi
# run the configured mutation tool
case "$MUTATOR" in
stryker) npx stryker run --mutate "$SOURCE_FILE" --reporters json ;;
mutmut) mutmut run --paths-to-mutate "$SOURCE_FILE" ;;
pit) mvn test org.pitest:pitest-maven:mutationCoverage ;;
esac
rm -rf "$BASE"
典型的调用方式是将 agent 命令与审计脚本配对使用。开发者首先让 agent 编写套件,然后将结果传给脚本,存活下来的 mutant 就会说明一切。
your-agent "Write a thorough test suite for src/date_parser.py. Cover invalid input, leap years, timezone offsets, and empty strings." > tests/test_date_parser.py
./mutant_audit.sh https://github.com/example/repo.git src/date_parser.py tests/test_date_parser.py mutmut
mutator 的输出是一份存活 mutant 的列表,而这份列表才是真正的交付物。每个存活者都是 agent 的测试未能锁定的一种行为。一种常见模式是:agent 用不同的输入值测试 happy path,却从不测试 failure path,所以让函数在无效输入时抛出异常的 mutant 就会存活。另一种模式是:agent 断言的是输出格式而非解析后的值本身,所以让日期偏移一天的 mutant 就会存活。阅读存活者是一种快速了解 agent 实际对函数理解了多少的方式。
mutation score 同样可以作为 prompt 设计的反馈信号。如果 agent 的套件得分低于 30%,可能说明 prompt 对函数的描述过于模糊;更具体地指出边界情况的 prompt 会产生更好的套件。如果得分高于 70%,说明 prompt 风格有效,同一措辞可以复用到下一个函数上。这将 mutation testing 变成了一个 prompt 调优循环,比通常的试错法更严谨。
这种方法有真实的局限性。Mutation testing 计算成本高昂,一个拥有慢速测试套件的大型仓库即使在快速服务器上也需要数小时。这里的工作流假设的是一个小型的、自包含的函数以及一个快速的测试运行器,而非一个启动数据库的集成测试就会启动的单一巨型应用。拥有大型代码库的团队应该在单个模块而非整个仓库上运行审计。该脚本还假设 agent 能产生项目运行器可以执行的测试文件——这一点对于每种框架或语言并非都能保证。
第二个局限性是:mutation testing 衡量的是行为覆盖,而非语义质量。一个套件可能杀死每个 mutant,但仍然包含难以阅读、紧密耦合于实现细节或运行缓慢的测试。审计回答了一个问题——测试是否会注意到行为变化——其余的则留给 code review。将高 mutation score 视为免去 review 的许可证的团队会失望的。
实践建议是:在合并下一个 agent 生成的测试套件之前,对其添加 mutation audit。成本是几分钟的设置和一次免费服务端运行,而收益是知道这个绿色套件是否真的有意义。一个亲眼见过 mutant 在 agent 的套件中存活下来的开发者,永远不会再像以前那样信任绿色对勾了。
如果这个审计听起来有用,MonkeyCode 的免费服务端和当前的 token 配额是运行它的实用选择,但在将工作流绑定到它们之前请查看项目的当前条款。绿色套件是最容易的部分;真正需要审视的是那些 dead tests。