AI 写的单测别急着合并:3 步接进 CI 防假绿

AI 写单元测试几乎零成本,但真正贵的是「覆盖率冲到 85%,线上照样翻车」。这篇给一套能接进 CI 的三步流程,把 AI 的产能和测试的有效性分开管。

为什么 AI 生成的测试经常「假绿」

模型的目标是让测试通过,不是让缺陷暴露。于是它会本能地走捷径:

  • 宽松断言expect(result).toBeDefined()assertIsNotNone、只断言「不抛异常」。
  • 过度 mock:把被测对象的内部逻辑也 mock 掉,最后测的是 mock 的返回值,不是你的代码。
  • 只造 happy path:空值、超长、负数、时区、并发这些分支一条不碰。

结果是 CI 全绿、覆盖率漂亮,回归照样漏。问题不在模型能力,而在于你把「写测试」和「定义什么叫做对了」一起交给了它。

第一步:先写契约,再让模型写测试

顺序反过来,质量差一个量级。

动手前先人工确定三件事:输入输出契约、必须抛错的分支、边界值清单。把它写成一段「测试规格」,和源码一起喂给模型。Prompt 里至少包含:

  1. 被测函数完整源码与调用方场景;
  1. 显式列出的边界(空、超长、负数、0、时区、重复提交);
  1. 硬性要求:每个测试必须有具体断言,禁止只断言「不抛异常」;
  1. 每个测试上方写一行注释,说明「这个用例在防哪种失败」——写不出来的直接删。

再加一个反向动作:让模型列出「这段代码最可能藏着什么缺陷」,然后针对这份清单补测试。这一步产出的用例质量,通常比直接生成高一截。

第二步:用变异测试筛掉假测试

覆盖率只说明代码被执行过,变异测试才能说明代码被验证过。

做法很直接:对被测模块注入变异(把 > 改成 >=、删掉一行、把返回值置空),再跑一遍测试。测试挂了说明变异体被「杀死」,没挂说明这块逻辑根本没被测住。AI 生成的测试杀伤率往往明显低于人手写的。

工具按语言挑:TypeScript/JS 用 Stryker,Java 用 PIT,Python 用 mutmut 或 cosmic-ray,Go 用 go-mutesting。

两个实操要点:

  • 只跑改动涉及的文件,别全局跑,否则十分钟的流水线会变成一小时;
  • 阈值分级:一般模块先按 50% 准入,支付、权限、金额计算这类核心路径按 80% 起,之后每月上调一次。阈值是往上走的,不是一步到位的。

第三步:CI 只卡变更行,断言交给人审

总覆盖率门禁在老项目里基本等于摆设——历史债压着,谁都不敢卡。改成卡新增/变更行覆盖率,历史代码不动就不背锅。

PR 里自动贴三样东西:新增测试清单、变更行覆盖率、变异分变化。评审时按 checklist 过一遍断言:

  • 是否覆盖了异常路径,而不只是正常返回?
  • 有没有同义反复的断言(expect(x).toBe(x) 这类)?
  • mock 层数是否超过两层?
  • 断言里有没有出现硬编码的「魔法期望值」,而不是从契约推导出来的?

再补一条团队规则:AI 生成的测试文件必须在 PR 描述里标注,并且至少有一名成员明确点过「我读过断言」。这条看着形式主义,但它把责任从模型挪回了人。

落地清单

  • 建一个 prompts/test-spec.md,把契约模板沉淀下来,别每次重新描述;
  • 把变异测试写成脚本,本地可跑,别只活在 CI 里;
  • 每月复盘一次「假绿逃逸」:哪个线上 bug 是测试没拦住的,补进用例并加进规格;
  • 记住分工:AI 负责写测试的体力活,你负责定义什么叫做对了。

覆盖率是给管理层看的,变异分是给三个月后的自己看的。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...