跳到主要内容

覆盖率幻觉:为什么 AI 生成的测试会继承代码的盲点

· 阅读需 10 分钟
Tian Pan
Software Engineer

一位小团队工程师花了三个月将测试生成委托给 AI。代码覆盖率从 47% 跃升至 72%,再到 98%。每次 PR 都返回绿色。然后生产环境崩了。用户注册中的竞态条件因数据库复制延迟导致重复邮箱。优惠码接口在代码无效时返回 null 而非零,导致支付计算对 4700 名客户静默出错。最终损失:4.7 万美元退款和 66 小时工程时间。测试并没有遗漏几个边界情况——它们覆盖了所写的代码,而非所部署的系统。

这就是覆盖率幻觉。随着 AI 辅助开发成为默认选项,落入这个陷阱正变得越来越容易。

行覆盖率真正衡量的是什么

行覆盖率是一个代理指标。它告诉你在测试运行期间哪些行被执行了,而不是这些执行是否验证了任何有意义的内容。一个测试可以触及函数的每一行,同时对函数输出是否正确毫无断言。

当你应用变异测试时,覆盖率与质量之间的差距就会显现。变异测试向代码引入小的刻意修改——将 > 翻转为 >=,将 + 换为 -——然后针对每个变异体运行测试套件。具有真正缺陷检测能力的测试套件会杀死大多数变异体;当行为改变时,测试失败。而覆盖率表演式的测试套件则让变异体存活。

一个有据可查的案例:一个测试套件实现了 100% 行覆盖率,变异得分为 4%。它执行了代码库中的每一行,却只捕获了 4% 的变异测试引入的缺陷。其余 96% 的变异——真实的行为变化——未被检测到,因为测试在检查代码是否运行,而非代码是否正确

这种差距并不是新问题。但 AI 生成的测试以一种特定方式使其在结构上更加严重。

封闭循环问题

当同一个模型同时生成你的实现和测试时,两个产物共享同一个关于代码应该做什么的心理模型。实现编码了假设,测试验证了相同的假设。如果假设是错误的,两者会以相同的方向出错。

考虑一个具体例子。一个实现在每次处理请求时递增计数器。一个测试验证计数器达到了预期数量。实现和测试都没有质疑的是:请求是否真正成功了。无论如何,计数器都会递增。代码和测试都基于"计数器递增等于成功"这一前提运作。这个前提从未从封闭循环内部受到质疑。

这不是语法或代码质量的失败。实现看起来是正确的,测试看起来是正确的,两者都能干净地编译和运行。失败发生在共享模型的层面:AI 从来没有理由质疑自己的假设是否有效,实现和测试都没有促使它这样做。

学术研究将此形式化为测试预言问题。LLM 生成的测试预言——测试断言的预期输出——捕获了代码所做的而非应该做的。当代码错误时,预言继承了错误。一项针对 Defects4J 基准中 17 个 Java 项目的 2024 年实证研究发现,GPT-4 的测试编译成功率为 52.96%,远低于 Evosuite 的 85.71%。在失败中,30.68% 是未解析的符号,17.25% 是参数不匹配——幻觉直接烘焙进了测试产物。

生产中的三种失败模式

覆盖率幻觉以三种不同模式显现,每种都比上一种更难捕获。

断言实现而非行为的测试。 AI 生成的测试默认断言特定函数以特定参数被调用——模拟被调用、返回值匹配记录的输出、计数器改变。这验证了实现按原样运行,而非行为是否正确。这是测试版本的校对自己的写作:你读的是你打算写的,而非页面上实际有的内容。测试和代码共享作者的盲点。

加载中…
Let's stay in touch and Follow me for more thoughts and updates