关于 mutation-testing
通过对生产代码施加变异并检查测试结果,评估测试能否发现有意义的代码变化。该技能侧重于 PR 准备阶段使用 Stryker 自动检测、分类存活变异,并据此改进测试。
主要功能
- 01说明变异生成与测试评估流程
- 02优先采用 Stryker 自动化变异测试
- 03将 PR 阶段检测与 TDD 循环分开
- 04分类存活变异,包括等价变异
- 05指导针对有价值存活变异补强测试
适用场景
- 01检查测试是否保护已变更行为
- 02发现高代码覆盖率掩盖的测试缺口
- 03在提交 PR 前评估测试
- 04针对存活变异强化断言
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill mutation-testing安装后在 Claude Code 中输入/mutation-testing确认已加载。
附带文件resources/mutator-rules.md
详细介绍
变异测试
若要编写优秀的测试(工厂、行为驱动模式),请加载 testing 技能。此技能侧重于验证测试是否有效。
变异测试回答了这样一个问题:“我的测试真的能捕获错误吗?”
代码覆盖率会告诉你测试执行了哪些代码。变异测试则会告诉你,测试是否能检测到代码中有意义的变化。仅有完整的行覆盖率,并不能证明断言保护了行为。
**默认做法:**优先使用自动化变异测试工具。对于 JavaScript 和 TypeScript 项目,如果尚未配置 Stryker,建议从 Stryker 开始。仅在无法使用自动化工具时,或将手动/思维变异作为教学辅助,或针对难以判定的存活变异进行重点跟进时,才使用这种方式。
深入学习资源位于 resources/ 目录中。按需加载:
| 资源 | 加载时机…… |
|---|---|
mutator-rules.md | 规划测试、扫描变更代码以查找可能的缺口、手动应用变异,或解读存活/等价变异时 |
核心概念
变异测试流程:
- 生成变异体:在生产代码中引入小错误(变异)
- 运行测试:针对每个变异体执行测试套件
- 评估结果:如果测试失败,该变异体即被“杀死”(好);如果测试通过,该变异体就“存活”(不好——测试未能发现错误)
要点:存活的变异体代表测试无法捕获的错误。
何时使用此技能
在以下情况下使用变异测试分析:
- 一个已完成的工作阶段已准备好提交 PR
- 审查分支上的代码变更
- 在 TDD 和重构后验证测试是否有效
- 找出看似有覆盖率、实则薄弱的测试
- 找出缺失的边界情况测试
- 验证重构没有削弱测试套件
与规划和 TDD 的集成:
FOR EACH TDD INCREMENT:
├─► CONFIRM: Human approves observable acceptance criteria
├─► RED: Write failing test, using mutator rules to spot likely gaps
├─► GREEN: Make it pass
├─► REFACTOR: If valuable
└─► Repeat without running the mutation harness
END-OF-PHASE PR-READINESS GATE:
├─► BASELINE: one full run for the accumulated branch/PR scope - the only whole-project run
├─► KILL MUTANTS: Strengthen tests for worthwhile survivors
├─► RE-RUN SCOPED ONLY: `--mutate <file>:<startLine>-<endLine>`, `--incremental`, or the diff
│ script - never repeat the whole-project run to recheck one file, one edit, or one config fix
└─► CLOSE: at most one final full run, present the triaged report, proceed to PR verification
自动化变异测试工具有意不纳入 RED-GREEN-REFACTOR 内循环。不要在每次测试、增量、重构或提交后运行它:随着代码库增长,运行成本也会增加,使短反馈循环逐渐变慢。在 RED 阶段,使用变异规则,以较低成本选择有力的示例。等实现和重构阶段完成、其他方面也已准备好提交 PR 时,再运行自动化工具。
一旦进入 PR 就绪检查阶段,就按常规变异测试流程进行:对完整报告进行分类处理,为有价值的存活变异体添加或强化行为测试,并重新运行针对性变异,直至它们被杀死或完成分类。这些针对性重跑属于同一个检查阶段,并不意味着回到每个增量都运行变异测试。
优先使用自动化工具的变异测试工作流
在阶段结束时的 PR 就绪检查中,只要实际可行,就使用 Stryker 验证测试是否有效。不要止步于推理测试是否能捕获某种变异;应针对已累积的变更运行自动化工具,再依据报告集中改进测试。
第 1 步:检查配置和范围
rg --files | rg '(^|/)(package.json|stryker\.config\.(mjs|cjs|js|json)|stryker\.conf\.(js|json))$'
git status --porcelain
git diff <review-base>...HEAD --name-only
- 确认包管理器、测试运行器、受影响的包,以及现有的 Stryker 配置。
- 使用实际的审查范围:对于单个 PR,使用检测到的默认分支;对于堆叠式 PR 层,使用紧邻其下的分支。
- 有意限定为差异范围的变异测试仅涵盖分支上已提交的变更。运行前要求工作树干净。如果
git status --porcelain的输出不为空,请停止并说明暂存、未暂存和未跟踪的工作均未纳入;不要擅自提交或暂存,也不要声称结果涵盖了完整差异。 - 对于堆叠式切片,针对父分支测试重点层。顶层还要运行
stack-pull-requests所要求的累积验收检查和仓库级检查。 - 在单体仓库中,先从受影响范围最小的包开始;目标运行正常后,再扩大到仓库级命令。
- 如果 JS/TS 项目未配置 Stryker,建议先添加配置,再进行手动变异分析。
步骤 2:缺少 Stryker 时进行设置
首先,根据官方文档以及仓库的运行时和测试运行器限制,选择兼容的、确切的 Stryker 软件包版本。添加依赖项和运行初始化程序都会更改仓库,因此必须获得与其他依赖项或设置变更相同的授权。然后使用仓库的包管理器指定经过审核的版本,例如:
pnpm dlx create-stryker@<reviewed-version>
然后检查并调整生成的 stryker.config.*:
- 如果有可用的项目测试运行器插件,优先使用它(
vitest、jest、mocha等)。只有在没有更紧密集成的实际可行方案时,才使用通用命令运行器。 - 只对第一方生产代码进行变异。排除测试、夹具、快照、生成文件、声明文件、构建输出、迁移文件和信号较弱的 barrel 文件。
- 对于 TypeScript,可考虑使用
@stryker-mutator/typescript-checker,这样类型无效的变异体会作为编译错误报告,而不是浪费测试时间。 - 首次设置时不要填写
thresholds.break值。没有实测基线时,任何数值要么会在第一天就导致构建失败,要么只是装饰;应将break留空或设为null,报告首次运行的得分,并且只在得分确定后再提出阈值。 - 保持设置变更便于审核:依赖项、配置和脚本;此外,每当在仓库中首次添加 Stryker 时,还应在
.gitignore中添加其临时文件和报告输出目录(.stryker-tmp/和配置的报告目录,通常为reports/),以确保运行不会留下未跟踪的产物。
步骤 3:推荐实用命令
添加全部三个脚本——全项目、缓存(--incremental)和分支差异——即使请求中只提到其中一两个也要添加;缓存脚本能让门禁的限定范围重跑更加经济。命名应与项目或请求已有的命名约定一致:
{
"scripts": {
"mutation": "stryker run",
"mutation:incremental": "stryker run --incremental",
"mutation:diff": "node scripts/stryker-diff.mjs <review-base>"
}
}
mutation:diff 辅助脚本应:
- 读取基准分支参数;如果未提供,则使用检测到的仓库默认分支。
- 当
git status --porcelain输出非空时拒绝运行,并说明<base>...HEAD不包含已暂存、未暂存和未跟踪的工作。 - 使用
git diff --name-only -z --diff-filter=ACMRTUXB <base>...HEAD收集变更文件,并解析以 NUL 分隔的记录;文件名可能包含空格或换行符。 - 保留与项目源代码扩展名匹配的变更生产文件。
- 排除测试/spec 文件、夹具、快照、生成文件、声明文件和构建输出。
- 不使用 shell 调用仓库本地的 Stryker 二进制文件。如果其
--mutate选项要求以逗号分隔的模式,则在拼接前清楚地提示并拒绝包含逗号的候选文件名;绝不能悄悄更改范围。 - 没有变更的生产文件可供变异时,清楚地退出。
使用简短的 Node 辅助脚本,不要在 package.json 中编写复杂的 shell 命令:命令替换无法保留以 NUL 分隔的路径,而在不同 shell 中对 *、!、逗号和换行符进行引用也很脆弱。
当报告指出特定的存活变异体时,使用精确的行范围进行小规模后续检查:
pnpm exec stryker run --incremental --force --mutate src/example.ts:42-57
使用仓库实际采用的包管理器;示例仅使用 pnpm 来展示仓库本地二进制文件。不要让执行命令悄悄下载版本不断变化的 Stryker 发布版。
第 4 步:运行与分类
在 PR 就绪门槛处,先运行 mutation:diff 获取分支反馈。引入 Stryker、更改共享测试基础设施、准备 CI 门槛或验证大范围强化测试时,才需要运行整个项目的 mutation;而且门槛阶段最多运行两次:一次是在更改任何测试之前运行基线,另一次是在更改后进行验证。其他所有运行都应限定范围(--mutate <file>[:<start>-<end>]、--incremental 或 diff 脚本),包括重新检查配置编辑或刚强化过的单个文件。
在花费基线运行次数之前,先证明测试框架能运行。编写或更改 Stryker 配置后的首次调用,应针对一个小文件进行限定范围的冒烟测试(--mutate <file>[:<start>-<end>]),绝不能运行整个项目:这只需几秒钟,就能发现会在测量任何变异体之前中止运行的问题,例如缺少 runner 插件、沙箱副本无法读取的路径、端口受阻或权限不足。统计尝试次数,而不是结果:即使调用崩溃、被终止或未生成变异报告,也已占用两次运行中的一次。此类中止后,先修复原因,再通过限定范围的冒烟测试重新验证,然后才运行整个项目。
绝不要为了重新读取测试框架自己的输出而再次运行它。基线运行已经生成幸存变异体列表:将其捕获一次(... 2>&1 | tee mutation-baseline.log,或添加 json 报告器并读取该文件),然后根据副本进行分类。为了 grep 幸存变异体而重新运行整个项目,会耗费一次完整运行,却不会带来任何首次运行没有的信息。
在门槛关闭前,对每个幸存变异体进行分类,并将这些类别纳入交付的报告中——即使你停下来询问是否继续,也要这样做。逐个检查幸存变异体,首先提出等价性问题:是否存在某种输入会让该变异体返回不同的值、持久化不同的状态或发出不同的消息?如果不存在,它就是等价变异体:例如,移除后结果不变的保护条件、+ 0 或 * 1 这样的恒等操作、多余的提前返回或不可达分支。任何测试都无法杀死这类变异体;记录一行理由即可。只有通过此项检查的幸存变异体,才需要考虑将其杀死或暂缓处理。因此,每个幸存变异体最终都必须且只能归入以下一种情况:被新测试杀死、附上理由并判定为等价,或作为需要判断的事项明确命名并暂缓处理。即使你尚未杀死任何变异体、只是在汇报,也要进行等价性检查:提出要杀死每个幸存变异体,就等于声称它们都不等价;这项判断必须逐个检查,而不能通过省略分类来作出。只列出幸存变异体而不分类,不算经过分类的报告;把每个幸存变异体都当作缺陷,也不算。
对 Stryker 的发现进行分类:
| 类别 | 说明 | 所需操作 |
|---|---|---|
| 已杀死 | 应用变异体后测试失败 | 无——测试有效 |
| 幸存 | 变异体生效时测试仍通过 | 添加或强化测试,除非该变异体等价 |
| 无覆盖 | 没有测试执行这段代码 | 添加行为测试 |
| 等价 | 变异体产生相同的行为 | 无——这不是真正的缺陷 |
立即修复明显问题:
- 缺少边界测试
- 断言薄弱或缺失
- 分支覆盖不完整
- 缺少副作用验证
- 高价值业务规则,例如金额、权限、资格、安全或数据丢失
对于需要人工判断的细微幸存变异体,如果当前测试框架提供结构化提问功能,则使用该功能;否则,提出一个简洁的纯文本问题。给出具体选项,说明变异内容,并描述权衡。在行为有意未作规定、正确的领域规则不明确、测试代价高昂或容易变得脆弱,或你不确定该变异体是否等价时,使用这种方式。
步骤 5:使用 TDD 消灭存活变异体
对于每个值得消灭的存活变异体:
- 保留或重新创建该变异体。
- 编写最小的行为测试,确保测试因正确原因在该变异体上失败。
- 恢复原始生产代码。
- 验证新测试通过。
- 将 Stryker 的范围限定为被变异的文件或行范围后重新运行,然后再次运行差异命令。
避免让测试过度依赖实现细节。强有力的变异测试会断言可观察到的行为:返回值、持久化状态、发出的事件、权限、消息或有意义的协作者调用。
Stryker 配置指南
Stryker 应作为 JS/TS 变异测试的常规入口。
初始配置
优先使用 stryker.config.mjs 或初始化程序生成的格式。使用 Vitest runner 需要同时安装 @stryker-mutator/vitest-runner 和 @stryker-mutator/core。典型的初始配置如下:
export default {
testRunner: "vitest",
coverageAnalysis: "perTest",
reporters: ["html", "clear-text", "progress"],
mutate: [
"src/**/*.{ts,tsx,js,jsx}",
"!src/**/*.test.{ts,tsx,js,jsx}",
"!src/**/*.spec.{ts,tsx,js,jsx}",
"!src/**/*.d.ts"
]
}
根据项目调整 testRunner、mutate、vitest.configFile、构建命令和 checker 插件。不要在布局不同的仓库中照搬此配置。
Vitest Browser Mode 注意事项: Stryker 的 Vitest runner 面向基于 Node 的测试项目,而非 Browser Mode 项目。如果仓库使用 Browser Mode 是因为被测声明需要真实浏览器行为,且支持情况与成本合适,可将 mutate 限定为由 Node 测试覆盖的非 UI 源代码,或将 Stryker 指向多项目 Vitest 设置中的 Node 项目。请先查阅 Stryker 文档,确认当前对 Browser Mode 的支持情况,不要想当然地认为 UI 包可以进行变异测试。
CI 与质量门槛
- 如果现有测试套件中有许多存活变异体,请先使用仅生成报告或仅检查差异的变异检查。
- 建立切实可行的基线后,再添加失败阈值。
- 将 HTML 和纯文本报告保存为 CI 构建产物。
- 使用增量模式加快本地反馈,但应定期强制执行完整运行,以避免基于过时假设。
- 将变异分数视为信号,而非虚荣指标。优先处理变更代码和高风险代码中的存活变异体及未覆盖变异体。
手动变异备选方案
如果无法使用 Stryker,或无法将其作用范围限定到正在审查的代码,请加载 resources/mutator-rules.md 并手动应用相关运算符。每次应用变异后,务必在进行下一次变异前还原。手动变异仍应遵循相同的循环:应用变异、运行测试、分类、修复明显的缺漏、就需要判断的问题征求意见,并报告结果。
总结:变异测试思维方式
针对每一行代码,关键问题是:
“如果我在这里引入了一个错误,我的测试能发现吗?”
对于每个测试,请验证它能否发现以下问题:
- 算术运算符变更
- 边界条件偏移
- 布尔逻辑反转
- 语句被移除
- 返回值变更
请记住:
- 覆盖率衡量代码执行情况,变异测试衡量错误检测能力
- 不包含断言的测试无法消灭变异体
- 边界值、混合布尔条件、非恒等值和可观察到的副作用,能消灭许多常见变异体
- 完整的变异运算符清单和示例请加载
resources/mutator-rules.md