AI生成测试覆盖率100%却漏掉96%的Bug

AI生成的测试套件常报告100%覆盖率却变异得分仅4%,覆盖率只证明代码跑过,不证明逻辑被检查。
文章通过一个AI生成测试套件的变异测试案例揭示:100%行覆盖率下,变异得分仅4%,96%的注入Bug未被捕获。原因在于行覆盖率只统计代码是否被执行,完全忽略断言是否有效。更严重的是,当同一模型既写实现又写测试时,双方盲区完美对齐,导致测试把错误假设当作正确证据。文章还描述了“结构上不可能失败的检查”——如对播放中的视频做截图哈希、依赖不存在的命令——这类检查永远通过,从未被审查。作者提出的核心判断标准是:如果我引入这个Bug,它会失败吗?无需工具链即可筛掉大部分纸糊测试。

有一支团队对一个AI生成的测试套件做变异测试,报告里写着100%行覆盖率——每一行代码都被执行过。但变异得分——也就是那些故意注入的Bug被测试真正抓住的比例——是4%。换句话说,96%被注入的缺陷从一套“完全覆盖”的测试里大摇大摆地走了过去。
这个数字比“AI会幻觉”那句老话要尖锐得多。覆盖率告诉你代码跑过,它完全不说明任何东西被检查过。
为什么覆盖率会说谎
行覆盖率的统计方式本身就有漏洞:一条语句只要在测试函数里被执行了,它就算作“已测试”——哪怕后面跟着的断言是 expect(result).not.toBeNull(),而那个函数根本不可能返回 null。
Thoughtworks 的技术雷达早就点出过这个问题:高覆盖率数字经常掩盖逻辑空洞的测试,AI生成的测试尤其如此。
这背后有个结构性原因。当同一个模型既写实现又写测试时,两边继承的是同一套盲区。HackerNoon 上有一位实践者描述过这样的场景:一个计数器无论请求是否成功都会自增,而测试套件把“计数器涨了”当作请求成功的证据——因为这个假设就写在它模仿的那段代码里。Bug 和测试互相点头,没有谁提出异议,所以没有任何一条测试变红。
变异测试是反向校验:注入一个小故障(翻转比较符、删掉一行、改掉一个常量),然后重新跑测试。如果没有测试失败,这个变异体就“存活”了——一个存活的变异体意味着某类真实Bug可以在全绿测试的掩护下上线。在一份非正式报告里,20个针对 Laravel 控制器写的AI测试宣称覆盖了全部端点,但在95个注入变异体中只杀死了68个——27个Bug躲过了“完全覆盖”。
变异测试(Mutation Testing)是一种评估测试套件有效性的技术。它的核心思路是:故意在源代码中注入微小的人工缺陷——称为“变异体”(mutant),比如翻转一个比较运算符、删除一行语句、替换一个常量——然后重新运行测试。如果某个变异体导致至少一个测试失败,说明这个变异被测试杀死了,意味着测试确实检查了相关逻辑;如果所有测试依然通过,这个变异体就存活了,说明测试套件对这类真实Bug完全无感。变异得分(Mutation Score)就是被杀死的变异体占注入总数的比例。相比行覆盖率只关心代码是否被执行,变异得分关心的是代码的正确性是否被断言验证。它本质上是在模拟真实Bug是否会被测试捕获,因此被认为是衡量测试套件质量的更严格标准,但代价是计算开销更高,因为每个变异体都需要重新跑一遍测试。
行覆盖率(Line Coverage)是最常见的代码覆盖率度量之一,统计的是测试执行期间源代码中至少被执行一次的语句行占总行数的比例。例如,一条 if 语句只要被进入过一次,它对应的行就算作已覆盖,无论测试是否检查了条件分支的结果。类似地,一个函数只要被调用过,函数体内的每一行都会计入覆盖率,哪怕函数返回值从未被断言验证。因此,高行覆盖率完全不能保证测试检查了任何逻辑——它只证明代码跑过。更精细的度量还有分支覆盖率(检查每个条件分支是否都被执行)、路径覆盖率(检查所有可能的执行路径组合)、以及条件覆盖率(检查复合条件中每个子条件的真值组合),但这些指标也仍然不能替代断言有效性的验证。
更极端的例子:一个结构上不可能失败的检查
有位读者在评论里描述了一种比弱断言更糟的情况:一个在结构上就无法失败的检查。
他们的例子是这样的:一个自动化移动端检查会在滑动前后对截图做哈希,用来确认信息流是否前进了。但如果屏幕上正在播放视频,哈希每一帧都在变——于是无论滑动有没有生效,这个检查都通过。第二个检查用 ps -o etimes= 读取进程存活时间,但这条命令在 macOS 上根本不存在;命令静默失败,存活时间永远不越过阈值,规则也永远不会触发。两个检查从来没有变红过,所以也从来没有被送去审查。
他们提出的那个问题值得直接抄走:如果我关心的那件事没有发生,这个检查还会通过吗?
这就是变异测试,一句话版本,只不过是用手而不是用工具来做的。
假阳性(False Positive)是指测试报告失败但实际并没有Bug,而假阴性(False Negative)是指测试报告通过但实际存在Bug——文章中描述的“结构上不可能失败的检查”正是假阴性的极端形式。这类检查的特点是:它的通过条件与它声称要验证的目标之间没有任何有效关联。例如,对动态内容(如正在播放的视频)做截图哈希比较,哈希值始终在变,无论滑动是否生效,比较结果都是“不同”,因此检查永远通过;再如,依赖一个在某些平台上不存在的命令(如 macOS 上不存在的 ps -o etimes=),命令静默失败,脚本从不报错,检查也永远不触发。这种检查比弱断言更危险,因为它伪装成有效的验证,通过了代码审查,却对真实故障零响应。识别它的方法就是文章提出的那个问题:如果我关心的那件事没有发生,这个检查还会通过吗?
可以立刻做的事
你不需要先装一个变异测试框架才能开始。在信任任何一个AI写的测试之前——不管是你自己写的还是同事写的——直接问那个问题:如果我引入这个测试本该抓住的Bug,它会失败吗?如果答案必须靠跑一遍才知道,那这件事本身就是审查项,而不是“它通过了吗”。
作者维护了一个免费的实验仓库,把这种对比做成了可以跑的东西。node run-lab.js weak 会针对故意写错的代码生成AI测试——3个测试全部通过。node run-lab.js verify-candidate 则用一套独立的验证套件跑同一份代码——它失败了,并且抓住了全部4个预埋Bug。两次运行之间的差异,就是这篇文章的全部论点,大约两分钟可以复现。
核心结论
覆盖率衡量的是代码有没有被执行,变异得分衡量的是测试有没有真正在检查。两者之间的缺口,就是AI生成测试最容易骗过人的地方——同一模型写实现和测试时,双方的盲区会完美对齐,于是没有一条测试会亮红灯。
判断一个测试是否可信,不需要先搭工具链。问一句“如果我制造这个Bug,它会失败吗”,就能筛掉大部分纸糊的测试。
相关推荐

DeepSeek开源推理框架Deepinflow:AI价格战打到后厨
DeepSeek开源内部推理框架Deepinflow,公开高性能算子、并行策略、推测解码等优化经验,直指OpenAI等巨头的高价API模式,用部署效率开源挑战AI定价逻辑,推动大模型推理从奢侈品走向公共基础设施。

DeepSeek千亿融资到账:AI日报第14期重点解读
DeepSeek千亿融资落地,宁德时代与腾讯注资;MiniMax开源428B模型权重,10家芯片厂接入;ARC浏览器宣布关停;PHP曝严重漏洞需立即升级;OpenAI估值下调、Anthropic捐出智能体标准;微软AI看病系统登《科学》。AI日报第14期重点全解读。

DeepSeek开源推理框架DeepInfer全解析:大模型推理优化揭秘
DeepSeek开源推理框架DeepSeek-Infer,本文基于B站UP主沐贤的分享,解析其核心优化技术:投机解码、一步调度、批次不变性、结构化输出等,并探讨开源范围与缺失组件,助你理解大模型推理优化实践。