打开网易新闻 查看精彩图片

92%代码覆盖率,SonarQube零告警,全绿通过。一个AI生成的去重bug就这么溜进了生产环境——因为没有任何测试真正挑战过那行逻辑。

这是上个月某支付公司的真实事故。140个单元测试,92%行覆盖,PR看起来漂亮极了。部署两天后,对账系统开始静默复制行项目。AI在对象比较时用了引用相等(==)而非业务键相等(.equals()),98%场景功能一致,2%从数据库重建对象的场景直接灾难。

所有测试只验证"去重发生了",没验证"怎么发生的":

assertEquals(3, result.size()); // 两种实现都能过

assertTrue(result.containsAll(expected)); // 测试数据里用的是同一批对象

把.equals()换成==,全部测试通过。这就是变异测试(Mutation Testing)要解决的问题。

覆盖率是"到过",变异测试是"能发现"

作者打了个比方:走进一栋楼,覆盖率意味着你逛遍了所有房间;变异测试意味着如果少了一堵墙,你能察觉。一个测"有没有",一个测"管不管用"。

代码覆盖率只告诉你哪些行被执行过。变异测试告诉你:如果这行代码错了,你的测试能不能抓住。

打开网易新闻 查看精彩图片

在AI生成代码的世界里,这是唯一重要的事。

人类写bug有套路——拼写错误、边界差一、空指针。测试体系这些年进化,专门抓这些。但大语言模型(LLM, Large Language Model)的bug是另一物种:结构正确,语义漂移。它没有你的领域概念,不知道"去重"在这个系统里等于"业务键相等",而非"对象身份"。

变异测试怎么工作:故意搞破坏

变异测试怎么工作:故意搞破坏

原理简单到近乎粗暴。拿你的代码,故意制造小破坏,看测试是否报警。

原始代码:if (a.getBusinessKey().equals(b.getBusinessKey()))

变异体1:if (a.getBusinessKey().equals(b.toString()))

异体2:if (a == b)

变异体3:if (true)

变异体4:if (!a.getBusinessKey().equals(b.getBusinessKey()))

打开网易新闻 查看精彩图片

如果测试对这些"破坏版"依然绿灯,变异体就"存活"了——你找到了测试盲区。这个盲区本来就存在,变异测试只是把它标在地图上。

变异分数 = 杀死的变异体 / 总变异体。60%意味着40%的行为路径未被测试,跟你的行覆盖率无关。

每个存活的变异体,都是一颗定时炸弹

每个存活的变异体,都是一颗定时炸弹

从可观测性角度看,这些存活变异体是"静默失败"的温床。你的日志和监控发现不了,直到48小时后下游对账报表爆炸。

变异测试能把平均检测时间(MTTD, Mean Time to Detect)压到部署前。

工具链已经成熟。Java有PIT,Python有MutPy,JavaScript有Stryker,Rust有cargo-mutants。IDE插件、CI/CD集成、增量变异(只测改动的代码)都齐了。运行成本从"喝杯咖啡"到"吃顿午饭"不等,取决于代码库规模。

但别急着全量跑。作者建议:核心领域逻辑先上,AI生成代码强制变异门禁,遗留代码增量覆盖。变异测试不是替代现有测试,是给覆盖率数字做"压力测试"。

那个支付公司的后续:他们在CI里加了变异门禁,AI生成代码必须80%变异分数才能合并。三个月后,生产环境同类bug归零。不是AI变聪明了,是测试学会了问更刁钻的问题。

当你的测试开始质疑代码的每一个"理所当然",AI生成的代码才真正有了护城河。