AI 编码的测试证据门禁:为什么通过用例还不够
AI 写出的代码读起来都像能跑。有时候它甚至能通过自己写的那套测试。我现在的规矩是:不信代码,不信测试,只信"这些测试真的能抓住真实 bug"的证据。下面讲的,就是我在合并前如何把这份证据落成具体文件。
最吓人的是能通过评审的幻觉
我批过最吓人的一个 AI 生成 PR,是支付拒付处理器。测试全绿,diff 读起来干净利落,助手还顺手加了一个完整的测试文件贴在实现旁边。两周后我们发现,拒付路径在生产从来没执行过——处理器在一个被误读的状态码上提前返回,而测试把网关 mock 成永远返回批准。断言确实跑了,但断言的东西跟业务一点关系也没有。
这就是幻觉问题:AI 代码看一眼就过,因为它在模式匹配"长得像能跑的代码";它的测试也过,因为测试确认的正是代码已经在做的事。评审挡不住这种东西。真正的门禁是测试,而测试本身需要证据。
我在每个 PR 上都要求的三件证据
- 一次通过的运行日志。PR 描述里贴 CI 运行 ID 的链接,指向最终 commit 上的绿灯。要 URL,不要截图。
- 一次失败的运行日志。证明同一批测试在实现写错时真的会红:分支上早一点带 bug 的 commit,或者把某一行注释掉之后跑的一次。"我本地跑过"不算证据。
- 一次变异见证。在新代码上故意施加一处变异,再跑一次 CI,看套件是否能抓住。如果把
>=改成>所有测试还是绿的,那这些测试根本没在测。
这三件事合起来回答了唯一重要的那个问题:这些测试能不能在这个 diff 上区分正确与错误行为。光是 build 过了,什么都证明不了。
覆盖率表演以及如何识破它
行覆盖率是软件里被误用最多的指标。我审过 97% 行覆盖率但连除零都抓不到的套件。套路重复出现:
- 只有行覆盖率没有分支覆盖率。测试走完函数主干,从来没进过
else。 - 断言比较的是变量和自己、或者和代码里也返回的同一个常量。
- 测试实例化了一个对象,但根本没调用它声称要测的那个方法。
- Mock 设得太宽松,被测代码内部逻辑根本没机会跑。
expect(result).toBeDefined()。函数返回了某个东西。它是什么。
必须存在的四类测试
- 单元测试覆盖纯逻辑,包括那些没人愿意想的丑陋分支。
- 集成测试至少跨越一个真实边界:数据库、队列、文件系统、HTTP server。不是那个边界的 mock。
- 失败路径测试故意把依赖打坏。超时、500、畸形 payload、磁盘写满。代码里有
catch,就必须有东西往里面扔异常。 - 验收测试用 Given/When/Then 表达,从外部驱动,覆盖工单最初描述的用户可见行为。
机器能校验的 Given/When/Then
我把验收标准固定写在 PR 描述里,格式如下,写不出来我就拒绝开始 review:
Given a customer with a valid card that the gateway will decline And an order in the pending state When the payment handler processes the charge Then the order moves to payment_failed And a decline email is queued And no capture request is recorded in the ledger And the response body contains the gateway decline code
四个 Then,四条断言,一一对应。如果验收测试文件里的断言数比 Then 少,那两边必有一边在撒谎。
当测试也是 AI 写的
助手有一切动机去写"刚好确认它自己写的代码"的测试。真正能用的防线只有几条:
- 对变更文件做变异测试。Stryker、mutmut、PIT,看你的语言有什么就用什么。只跑 diff 范围,新代码的变异分数掉到下限以下就让 build 红。一个活下来的变异体就是 AI 漏掉的一个缺口。
- 基于属性的测试用在任何接受结构化输入的地方。AI 会写一个带三行数据的示例测试;用一千个生成输入的 property test 会把 off-by-one 抠出来。
- 对抗性用例评审。我自己先写下三种 AI 大概率没想到的输入:空、巨大、畸形。然后去测试文件里找它们。第一版里几乎从来都没有。
评审清单与 CI 门禁
我的 reviewer 清单很短,每一条不过我就不批:
- 对每一条新加的断言,我能不能指出如果删掉它,会在实现的哪一行上失败?
- 如果我把内部变量重命名,测试还过吗?应该过。
- 如果我反转一条业务规则,至少有一个测试会红吗?最好会。
- 有没有真正的失败路径测试?不是那种名字叫 "handles errors" 但只检查 try/catch 存在的糊弄货。
- 测试文件的体量是不是大致和实现同步增长?还是实现 300 行、测试 40 行?
人会松懈,CI 不会。我的流水线会在这些情况下让 build 红:分支覆盖率下降、任何一行新的源码未被覆盖、源文件多了 50 行但测试文件没跟上、变更文件的变异分数掉到下限以下、或者 PR 描述里缺了指向 CI 运行 ID 的 Test-Evidence: 标签。最后那个门禁是我手里杠杆最大的一条——一个 bot 会在发现它指向某个三次 force-push 之前的陈旧运行时直接禁用合并。
Snapshot、flaky 以及其他谎言
Snapshot 测试是 AI 生成代码里最常见的覆盖率表演。助手调 toMatchSnapshot,第一次运行写下的 snapshot 就成了 oracle,从此这个测试永远确认组件第一次产出的那个结果,带 bug 也一起确认。Snapshot 只在有人逐行 review 每一处变更时才有用,所以我禁止用它测行为,只允许它用于纯格式 fixture。
Flaky 测试我是同一套粗暴政策:flaky 测试就是一次带着好 PR 的失败测试。一个工作日内修好或者删掉。永远不要 skip。被 skip 的测试会烂成"某个没人测的功能的文档",在绿色仪表盘上一点也看不见。如果那东西本质上就是概率性的,就在测试模式下把它变成确定的,或者挪到每晚跑的 soak 套件里单独报告。
我们强制推行之后发生了什么
头一个月挺难受。PR 吞吐量掉了下去,因为助手不停提那些过不了变异门禁的 PR。到了第三个月,通过我们的 prompt 和固定示例,助手已经学会在第一次就产出能在变异下存活的测试。那个拒付路径的 bug 没再出现过,因为"失败运行日志"这项要求在结构上就让"从没见过失败的测试套件"没法被合入主干。证据只要你去要就不贵。信任一旦丢了就很贵。
评审时看什么
这篇文章适合用在给 AI 代码设置测试证据门禁时。别从“原则”聊起,直接拿一条真实改动来对照,看看规格里还缺什么。
- 区分“跑过测试”和“测试证明了什么”。
- 要求 PR 附上失败路径、回归范围和手工验证结果。
- 把覆盖率数字和关键行为证据分开看。
- 没有证据的绿色 CI 不应该自动合并。
门禁的价值不是让流程变重,而是让评审者少猜一点:这段 AI 代码到底证明过什么。
证据门禁要看原始输出,不看截图
我更信一段完整命令输出,而不是一句“tests passed”。证据门禁最小可用版只有三样:命令、时间、完整输出。AI PR 如果只贴绿色截图,reviewer 看不到过滤条件、跳过用例和真实失败信息。
Evidence packet: - command: pnpm test --filter api-contract --runInBand - timestamp: 2026-04-28T09:40:12Z - commit: 62f9d1e - result: 38 passed, 0 skipped - extra check: openapi-diff old.yaml new.yaml returned non-breaking - missing: no load test; reviewer accepted because endpoint is read-only
边界:证据门禁不是让每个 PR 都跑全量回归。门禁应该和风险匹配。数据库迁移、队列消费、计费和认证要严;纯样式修复可以轻。
可复制产物:AI 编码评审包
在 AI 生成 diff 进入代码评审前使用。它把提示词范围、允许变更和证据要求合并成一个可审查产物。
AI 编码评审包:AI 编码的测试证据门禁:为什么通过用例还不够 本次要做的决策: - 确认 AI 只在批准范围内生成变更,并为每条验收标准提供证据。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: AI 边界:生成变更必须留在书面范围内,每条验收标准都要能找到证据。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
编辑复核记录
复核日期:2026-04-28。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。
专题阅读路径
这篇文章归入 验收标准 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
继续阅读
编辑说明与免责声明
最近复核:2026-04-28。编辑部检查了示例、内链和可复制评审片段,确保内容更适合真实项目使用。
本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。
- 作者信息:Spec Coding 编辑部
- 编辑政策:编辑与事实核查政策
- 联系方式:联系页面