变异测试在测什么
普通覆盖率回答「这行代码被执行了吗」,变异测试回答「测试有没有能力发现这行的逻辑被改坏」。工具自动对源码做微小改动(把小于号换成小于等于、删掉一次方法调用、对返回值取反),每产生一个变异体就完整跑一遍测试:测试失败称为变异被杀,测试仍然通过称为变异存活。存活意味着这处逻辑缺少有效断言。变异得分等于被杀变异数除以有效变异总数,用来衡量断言强度。
基本信息
| 原理 | 自动注入小缺陷,用测试集检验是否被发现 |
|---|---|
| 常见工具 | PIT(JVM)、Stryker(JS/TS、C#)、mutmut(Python) |
| 成本 | 约等于变异体数量乘以单次测试时长 |
| 适用 | 关键模块抽检,不做全仓例行 |
典型使用场景
- 核心计价与结算逻辑:断言写得多但强度未知,用变异得分验证是否真能拦住改动
- 覆盖率已高但线上仍有逃逸缺陷:说明执行到位而断言偏弱,用存活变异定位薄弱点
- 接手遗留模块:先跑一轮变异测试,快速找出测试形同虚设的区域
- 评审新增测试的充分性:对变更文件跑增量变异,看新断言是否覆盖了改动分支
- 测试套件重构后回归验证:确认精简用例没有削弱发现缺陷的能力
Stryker 最小用法
Stryker 最小用法
npx stryker run # 对配置范围内代码执行变异
npx stryker run --mutate src/pricing/*.ts # 只测目标模块
# 报告分三类:killed(被杀)/ survived(存活)/ timeout(超时)
# survived 的条目就是需要补断言的位置报告条目读法
| 状态 | 含义 | 正常与异常 |
|---|---|---|
| Killed | 测试因变异失败,说明断言生效 | 占比高是好事,是主要追求项 |
| Survived | 测试仍通过,变异未被发现 | 需逐条判断是否缺断言 |
| Timeout | 变异导致死循环或超时 | 少量正常,集中出现说明实现有问题 |
| No coverage | 该变异体所在代码从未被测试执行 | 属于覆盖缺口,先补用例 |
| Runtime error | 变异导致编译或运行错误 | 需要查看是否工具配置问题 |
不是所有存活变异都要修:等价变异体(改动后语义完全一致,例如把条件里冗余的括号顺序调整)天然无法被杀,属于噪声。处理时先判断这处变异的语义是否真的改变了。
成本控制是能否长期使用的关键。按模块轮换抽检、只对变更文件做增量变异、给测试集设超时,都能把单次运行压到分钟级;全仓库定时跑通常不划算。
落地检查清单
- 先看存活变异的分布,再决定补断言还是调整设计
- 对关键模块设定得分下限,作为门禁的一项
- 把等价变异体登记进忽略清单,避免每次重复判断
- 记录得分随时间的变化,而不是只看单次绝对值