别轻信 Agent 生成的测试
原文由 Alex O'Callaghan 于 发布,订阅该博客
我用 Agent 将 53 个 Enzyme 测试套件迁移到了 React Testing Library。测试全部通过,代码看起来也很规整,但仔细检查后发现,有些测试根本无法捕获回归问题,有的断言对象错了,还有的几乎什么都没测到。
对于 Agent 生成的测试,这类问题很容易被忽略,因为输出看起来很精致,代码整洁,而且数量庞大。
从 Enzyme 到 RTL 的迁移
这些 Enzyme 测试作为技术债,以低优先级的身份在待办清单里躺了好几年。这恰恰是 Agent 应该擅长的那种定义清晰、机械化的工作。我按复杂度进行了拆分,用自己的 prepare-mr-skill 来组织提交、撰写说明,结果似乎很成功。至少表面上看是这样:测试都迁移完了,全部通过,代码改动看起来也合情合理。
合并前,我们按常规的团队评审流程审查了这些 MR,发现有些测试看似正确,却无法捕获回归。例如,有一个测试断言某个特定 ARIA 角色的元素未被渲染,但该角色本身是无效的,组件原本就不可能渲染出这样的元素。还有的测试混淆了 disabled 和 aria-disabled,这两个属性行为不同,对无障碍访问都很重要,但外表足够相似,以至于一个看似合理的测试即使写错了也不会失败。
为什么会这样
这个问题并非 Agent 独有,手动编写测试也很容易写出这样的用例。不过,人写出糟糕的断言时,通常对组件比较熟悉,可能会觉得这个测试过得太轻松。而 Agent 没有直觉,它只是根据实现做模式匹配,拼凑出一个看起来正确的结果就继续往下走了。再加上 Agent 能一次性产出大量代码,这类问题就更容易漏网。
当实现已经存在时(例如迁移测试、补齐覆盖率、验证几年前编写的测试),测试驱动开发(TDD)天然的保障就不起作用了。你是在针对已经能工作的代码编写测试,所以错误的断言根本没有机会失败。唯一的发现办法就是故意把组件改坏:临时修改组件,确认测试能否捕获问题,再改回来。
如何让校验真正生效
我的第一反应是修改提示词,强制让 Agent 检查这类错误。我更新了 RTL 迁移 skill,明确要求每个测试都必须在失败状态下验证通过。
但这并不可靠。Agent 会考虑这个要求,偶尔能发现一些问题,然后就继续往下做了。它把校验当成了待办清单上的勾选项,而不是硬性约束。
部分原因在于结构性问题。当你让 Agent 一次性迁移大量测试时,任务很长,上下文会被塞满。随着上下文变长,模型会开始失去焦点,不再关注你的指令。有些模型还会表现出“上下文焦虑”,在接近上下文窗口末尾时会提前收尾。这会导致 Agent 开始遗忘或跳过这类校验指令。
我尝试改为按测试套件逐个迁移,而不是一次性全量处理,心想更短的上下文能减轻完成压力,让 Agent 有更多余地在校验步骤上放慢节奏。Agent 确实更有可能去检查了,但往往只会决定抽查“几个关键测试”,而不是逐个验证。
最后,我决定把迁移和审查拆成两个独立的 Agent 任务。我写了一个 Bash 脚本,遍历每个测试套件,依次执行两个 Agent 提示:一个负责迁移,一个负责审查。
#!/bin/bash
echo "Starting migration..."
declare -a files=(
"path/to/test-suite.test.tsx"
)
for i in "${files[@]}"; do
echo "Migrating $i"
agent -p --force --output-format stream-json --stream-partial-output --model claude-4.6-sonnet-medium \
"Migrate $i to use RTL. Ensure linting passes and then commit your changes in a single commit."
agent -p --force --output-format stream-json --stream-partial-output --model claude-4.6-sonnet-medium \
"Review the RTL tests in $i -
* Identify any unnecessary tests, remove them
* Identify any tests not following best practices, fix them
* For every single test intentionally change the implementation to break the specific feature tested and confirm the test fails as expected, fix them if they don't
Ensure linting passes and then commit your changes in a single commit."
echo "Migrated $i"
done在校验任务更聚焦的情况下,Agent 会自己列出待办清单,逐个测试仔细过一遍,不仅捕捉到了问题,还做了一些超出人工审查所发现范围的改进。
代价
这个办法奏效了,但成本也高得多。所有迁移都是通过 Cursor 使用 claude-4.6-sonnet-medium 对 22 个测试套件批量执行的:
| 方案 | 成本 |
|---|---|
| 单次批量处理 | ~$13 |
| 按套件逐个处理 | ~$32 |
| 按套件处理 + 审查 | ~$62 |
按套件加审查的方案解决了之前人工审查发现的所有问题,但成本几乎是单次批量处理的 5 倍。
Agent 让我们能做更多事,完成那些原本只会一直躺在待办清单里的现代化改造。但要把事情做好、做到结果可信,成本比我们预想的要高。我的看法是,既然要做,就得做好,只不过即便所需时间大幅缩短,也需要在真实的金钱成本上做权衡。
组织需要有意识地思考,如何让生产力的提升真正转化为收入增长,而不仅仅是 AI 供应商账单上的成本增加。
启示
双提示词脚本的本质不是更好的提示词,而是不同的架构。当你让同一个 Agent 在同一轮里既做迁移又做校验时,任务结构本身就不利于校验。当某个流程步骤总是被丢掉时,解决办法通常不是写一条更好的指令,而是设计一个让这一步无法被跳过的工作流。
从概念上讲,这里审查者在做的就是变异测试:针对每个断言,去验证如果改动组件,测试是否会失败。在测试流水线中引入像 Stryker 这样的变异测试工具,有助于发现这类问题,还能避免让 Agent 手动去做,从而降低成本。
设计 Agent 工作流所花的时间也是工作的一部分,在评估 Agent 带来的生产力提升时应该计入在内。这决定了你得到的是值得信赖的产出,还是仅仅看起来可信的产出。
随机一篇博客
评论
登录后参与讨论