mcpField
M
mutation-testing

mutation-testing

github.com/laurigates测试与质量5966 次安装今天更新
代码仓库

关于 mutation-testing

mutation-testing 是用于评估测试能否发现错误的技能,方法是有意引入代码变异。它涵盖 TypeScript/JavaScript 的 Stryker 和 Python 的 mutmut,并说明如何解读存活变异体及识别手写测试框架的误导性结果。

主要功能

  1. 01解释变异体被杀死与存活的含义
  2. 02指导配置和运行 Stryker
  3. 03指导配置和运行 mutmut
  4. 04解读变异分数与存活变异体
  5. 05指出手写测试框架的常见陷阱

适用场景

  1. 01找出未能验证行为的测试
  2. 02识别缺失的断言和边界情况
  3. 03调查测试套件中的存活变异体
  4. 04评估自定义测试框架的变异结果

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/laurigates/claude-plugins --skill mutation-testing

安装后在 Claude Code 中输入/mutation-testing确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

REFERENCE.md
references/
hand-rolled-harness-failures.md
triaging-large-survivor-sets.md

详细介绍

变异测试

变异测试方面的专业知识——通过引入刻意的代码变异,验证测试是否确实能捕获错误。

何时使用此技能

在以下情况下使用此技能……在以下情况下改用其他技能……
验证测试的有效性编写单元测试(使用 vitest-testing)
查找薄弱或不充分的测试分析测试异味(使用 test-quality-analysis)
设置 Stryker 或 mutmut编写 E2E 测试(使用 playwright-testing)
提高变异分数生成测试数据(使用 property-based-testing)
检查测试能否捕获真实错误仅设置代码覆盖率

核心专业知识

变异测试概念

  • 变异体:自动引入的小型代码更改(变异)
  • 被杀死:测试在存在变异时失败(好——测试捕获了错误)
  • 存活:测试在存在变异时通过(不好——测试薄弱)
  • 覆盖率:测试执行了变异代码,却未能捕获变异
  • 分数:被杀死的变异体所占百分比(目标为 80% 以上)

变异测试揭示的问题

  • 未实际验证行为的测试
  • 缺失的断言或边界情况
  • 过于宽松的断言
  • 死代码或不必要的逻辑
  • 需要加强测试的部分

TypeScript/JavaScript(Stryker)

安装
# Using Bun
bun add -d @stryker-mutator/core

# For Vitest
bun add -d @stryker-mutator/vitest-runner

# For Jest
bun add -d @stryker-mutator/jest-runner
运行 Stryker
npx stryker run                                    # Run mutation testing
npx stryker run --incremental                      # Only changed files
npx stryker run --mutate "src/utils/**/*.ts"       # Specific files
npx stryker run --reporters html,clear-text        # HTML report
open reports/mutation/html/index.html              # View report
理解结果
Mutation score: 82.5%
- Killed: 66 (tests caught the mutation)
- Survived: 14 (tests passed despite mutation - weak tests!)
- No Coverage: 0 (mutated code not executed)
- Timeout: 0 (tests took too long)
示例:薄弱测试与强测试
// Source code
function calculateDiscount(price: number, percentage: number): number {
  return price - (price * percentage / 100)
}

// WEAK: Test passes even if we mutate the calculation
test('applies discount', () => {
  const result = calculateDiscount(100, 10)
  expect(result).toBeDefined() // Too weak!
})

// STRONG: Test catches mutation
test('applies discount correctly', () => {
  expect(calculateDiscount(100, 10)).toBe(90)
  expect(calculateDiscount(100, 20)).toBe(80)
  expect(calculateDiscount(50, 10)).toBe(45)
})
在 Vitest 运行器下的静态存活变异体通常是误报

静态变异体会修改在模块加载时运行的代码(常量映射、Zod 模式)。在 Vitest 4.1 中使用 @stryker-mutator/vitest-runner 10.0.0 时观察到:

ignoreStatic: true效果
跳过没有任何测试覆盖的静态变异体
不跳过有测试覆盖的静态变异体——这些变异体仅针对 Stryker 归因其覆盖的 1–2 个测试运行,这些测试往往来自最先导入该模块的无关文件

因此,静态变异体可能报告为存活,即使真正断言该代码的测试本应将其杀死。某个 Next.js 应用中,766 个存活变异体里有 384 个标记为 static: true,且 testsCompleted <= 5;排除这些变异体后,分数从 68% 升至约 79%。针对许多测试运行过的静态存活变异体则是真实问题(从 Zod 模式中删除 .trim() 后,该变异体在 195 个测试中存活;手动应用后,在全部 96 个相关测试中也都通过)。统计值得阅读的存活变异体:

jq '[.files[].mutants[] | select(.status=="Survived" and ((.static|not) or .testsCompleted > 5))] | length' reports/mutation/mutation.json

阈值 5 是根据那一个代码库得出的经验值,并非 Stryker 定义的数值。判断可疑静态存活变异体的另一种方法:手动应用变异并运行相关测试——如果测试套件失败,说明该存活是误报。

Python(mutmut)

安装
uv add --dev mutmut                    # Using uv
pip install mutmut                     # Using pip
运行 mutmut
uv run mutmut run                                          # Run mutation testing
uv run mutmut run --paths-to-mutate=src/calculator.py      # Specific files
uv run mutmut results                                      # Show results
uv run mutmut summary                                      # Summary
uv run mutmut show 1                                       # Show specific mutant
uv run mutmut apply 1                                      # Apply mutant manually
uv run mutmut html                                         # HTML report
理解结果
Status: 45/50 mutants killed (90%)
- Killed: 45 (tests caught the mutation)
- Survived: 5 (tests passed despite mutation)

手写测试框架报告的结果比 Stryker 和 mutmut 少

以上内容都假设使用了框架。很多实际的变异测试采用手写循环——应用变异、运行一条断言、捕获失败——通常是因为被测对象是生成器或构建器中的构建时检查,而不是单元测试套件。

这样的循环值得编写。但它会漏掉框架免费提供的一项关键记录:**Stryker 和 mutmut 会告诉你每个变异体是被哪个测试杀死的。**手写测试框架通常只报告有某个东西失败了,而“有东西失败”与“我正在测试的检查失败”无法区分。以下四种情况会导致问题——前三种在一次会话中观察到,第四种在另一次会话中观察到:

在信任手写测试框架的表格前,请阅读 references/hand-rolled-harness-failures.md:§1 前一个检查掩盖了被测检查,§2 选择的变异只有目标检查能发现,§3 修改表格后导入时派生的状态仍然过期,§4 修改过的文件从未被导入(产生误报 MISSED,文中附有各运行时的搜索路径表)。

对绿色表格的影响

会对每个变异都打印 CAUGHT 的测试框架,常被当作测试套件可靠的证明。但它只能证明更有限的事情:

全部显示 CAUGHT 的表格只能证明每个变异都被某条断言捕获。 它无法证明捕获变异的是你想要的那条断言,也无法证明任何单条断言 确实有能力触发失败。

两个成本很低的补充措施可以弥补大部分缺口:

  • 刻意加入一个本应漏掉的无操作变异。 如果表格里的每个变异都显示 CAUGHT,它和已损坏的测试框架无法区分;预期出现一次 MISS,就能区分二者。这个对称性同样成立,而这种控制并不能覆盖它:全部显示 MISSED 的表格同样无法和已损坏的测试框架区分;按预期报告 MISSED 的无操作变异,与因未加载真实变异而报告 MISSED 的情况并无二致。先检查 META/accept-all 行——如果一个硬编码的 raise 或 accept-all 未能让测试套件变红,这并非断言较弱,而是证明测试框架没有运行它所编辑的文件(§4)。
  • 断言错误消息,而不只是异常。 将变异与错误消息中的预期子字符串匹配,这样被掩盖的结果会被视为测试框架失败,而不是悄然通过。

变异分数目标

分数质量行动
90%+优秀保持质量
80-89%良好小幅改进
70-79%可接受聚焦薄弱环节
60-69%需要改进补充缺失的测试
< 60%较差需要大幅改进测试

Agent 式优化

场景命令
快速 TS 变异测试npx stryker run --incremental --reporters clear-text
定向 TS 变异测试npx stryker run --mutate "src/core/**/*.ts"
快速 Python 变异测试uv run mutmut run --paths-to-mutate=src/core/
查看存活变异uv run mutmut results | grep Survived
CI 模式npx stryker run --reporters json

详细示例、高级模式和最佳实践,请参阅 REFERENCE.md。

另请参阅

  • references/triaging-large-survivor-sets.md - 排查数千个存活变异:按未断言的行为聚类,以对抗性方式验证声称的源代码缺陷,并要求测试编写者重新运行 Stryker 作为门槛
  • vitest-testing - 单元测试框架
  • python-testing - Python pytest 测试
  • test-quality-analysis - 检测测试异味
  • api-testing - HTTP API 测试
  • agent-patterns-plugin:tool-result-traps - 对任何会影响操作的负面结果进行控制测试(§4 中的硬编码 raise 正是这种控制)

参考资料

评论

支持 Markdown