You can't trust agent tests

Alex O'Callaghan

你不能相信智能体测试

我使用了一个智能体,将 53 个 Enzyme 测试套件迁移到 React Testing Library。测试通过了,代码看起来也很连贯;然而,当我们仔细检查后发现,其中有些测试无法捕获回归,有些断言针对的是错误的对象,还有一些实际上什么都没有测试。

对于智能体生成的测试,这些问题很容易被忽略,因为产出看起来很完善,代码很整洁,而且数量很多。

从 Enzyme 迁移到 RTL

这些 Enzyme 测试是积压在待办列表中多年的技术债务,优先级一直很低。这正是智能体理论上擅长的那类定义明确、机械重复的工作。我按复杂度对任务进行了拆分,使用我的 prepare-mr-skill 来组织提交并撰写描述,结果确实完成了。至少看起来是完成了:测试迁移了,测试也通过了,代码变更看起来合理。

在合并之前,我们按照团队惯常的评审流程检查了这些 MR,并发现了一些看起来正确、但无法捕获回归的测试。例如,有一个测试断言某个具有特定 ARIA 角色的元素没有被渲染,但这个角色本身无效,该元素原本就不可能由这个组件渲染出来。我们还发现有人混淆了 disabledaria-disabled:这两个属性的行为不同,对可访问性也很重要,但它们看起来足够相似,以至于一个看似合理的测试可能会把它们弄错,却仍然不会失败。

为什么会这样

这个问题并不是智能体独有的,手写测试时也很容易写出这样的测试。不过,手写错误断言的人通常了解这个组件,可能会注意到这个测试简单得不太对劲。智能体没有直觉,它会根据实现进行模式匹配,生成看起来正确的内容,然后继续往下做。此外,智能体能够生成大量代码,这也让类似的问题很容易蒙混过去。

当实现已经存在时(例如迁移测试、补充覆盖率,或验证多年前编写的测试),Test Driven Development(测试驱动开发,TDD)这一自然的保障机制就不适用了。你是在针对可正常工作的代码编写测试,因此错误的断言根本没有机会失败。捕获这类问题的唯一办法,就是故意破坏组件:临时修改组件,确认测试能够捕获这一变化,然后恢复修改。

让验证真正执行

我的第一反应是修改提示词,强制智能体检查这类错误。我更新了自己的 RTL 迁移 skill,明确要求每个测试都必须针对一个失败状态进行验证。

但这并不能稳定奏效。智能体会考虑这个要求,有时能发现问题,然后继续往下做。它把验证当成一个待勾选的事项,而不是一项必须满足的约束。

问题有一部分源于任务结构。当你要求智能体在一次处理中迁移大量测试时,任务会很长,上下文也会被填满。随着上下文变长,模型会开始根据你的指令失去专注。有些模型还会表现出“context anxiety(上下文焦虑)”:当它们接近上下文窗口末尾时,开始提前收尾。这会促使智能体逐渐忘记或跳过这类验证要求。

我尝试按测试套件逐个运行迁移,而不是一次性完成,心想较短的上下文窗口可以减轻完成任务的压力,让智能体有更多余地放慢速度执行验证步骤。智能体确实更有可能进行检查,但它往往会决定只验证“几个关键测试”,而不是逐一验证所有测试。

最后,我尝试把迁移和评审步骤拆成两个独立的智能体任务。我写了一个 bash 脚本,遍历每个测试套件,并依次运行两个智能体提示词:一个负责迁移,一个负责评审。

#!/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

在验证任务更加聚焦后,智能体列出了自己的待办事项,并逐一彻底检查了每个测试,发现了问题,还做出了一些超出评审中所发现范围的改进。

代价

这种方式确实有效,但成本也明显更高。所有迁移都通过 Cursor 使用 claude-4.6-sonnet-medium 完成,以下是其中 22 个测试套件的批处理结果:

方式成本
单次批处理约 13 美元
逐个套件处理约 32 美元
逐个套件处理 + 评审者约 62 美元

逐个套件进行评审的方式解决了人工评审之前发现的所有问题,但与单次批处理相比,成本几乎增加了 5 倍

智能体让我们能够完成更多工作,包括那些原本只会一直积压在待办列表中的现代化改造工作。但要把事情做好,并获得值得信任的结果,成本比我们预想的更高。我认为,既然要做一件事,就应该把它做好;不过,即使所需时间大幅减少,这项工作实际产生的金钱成本仍然存在权衡。

组织需要有意识地规划如何将生产力的提升转化为收入增长,而不是仅仅转化为 AI 供应商账单上的成本增加。

结论

这个双提示词脚本并不是更好的提示词,而是不同的架构。当你要求一个智能体在同一次处理中完成迁移和验证时,任务结构本身就不利于验证。当一个流程步骤总是被省略时,通常需要修复的不是指令,而是设计一种无法跳过该步骤的工作流。

从概念上说,评审者在这里做的是 mutation testing(变异测试):针对每一个断言,询问如果修改组件,该断言是否会导致测试失败。将 Stryker 这样的变异测试工具纳入测试流水线,或许能帮助捕获这些问题,并避免让智能体手动完成这项工作,从而降低成本。

设计智能体工作流所花费的时间也是工作的一部分,在评估使用智能体带来的生产力提升时应将其考虑在内。这决定了你的产出究竟是值得信任,还是只是看起来值得信任。

原文由 Alex O'Callaghan 发布

本文章由 gpt-5.6-luna 进行翻译