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生成的代码才真正有了护城河。
热门跟贴