名称: 突变测试 描述: 使用Stryker(TypeScript/JavaScript)和mutmut(Python)进行突变测试以验证测试有效性。找出即使代码发生突变也能通过的弱测试。用于提高测试质量。 允许工具: Bash, Read, Edit, Write, Grep, Glob, TodoWrite
突变测试
突变测试的专家知识 - 通过引入故意的代码突变来验证您的测试是否真正捕获错误。
核心概念
- 突变体: 自动引入的小型代码更改
- 杀死: 测试因突变而失败(好 - 测试捕获了错误)
- 存活: 测试在突变后通过(坏 - 弱测试)
- 分数: 杀死的突变体百分比(目标80%+)
TypeScript/JavaScript (Stryker)
安装
# 使用 Bun
bun add -d @stryker-mutator/core @stryker-mutator/vitest-runner
# 使用 npm
npm install -D @stryker-mutator/core @stryker-mutator/vitest-runner
配置
// stryker.config.mjs
export default {
packageManager: 'bun',
reporters: ['html', 'clear-text', 'progress'],
testRunner: 'vitest',
coverageAnalysis: 'perTest',
mutate: ['src/**/*.ts', '!src/**/*.test.ts'],
thresholds: { high: 80, low: 60, break: 60 },
incremental: true,
}
运行 Stryker
# 运行突变测试
bunx stryker run
# 增量模式(仅更改的文件)
bunx stryker run --incremental
# 特定文件
bunx stryker run --mutate "src/utils/**/*.ts"
# 打开 HTML 报告
open reports/mutation/html/index.html
示例:弱测试
// 源代码
function calculateDiscount(price: number, percentage: number): number {
return price - (price * percentage / 100)
}
// ❌ 弱: 即使我们突变计算,测试也能通过
test('应用折扣', () => {
expect(calculateDiscount(100, 10)).toBeDefined() // 太弱了!
})
// ✅ 强: 测试捕获突变
test('正确应用折扣', () => {
expect(calculateDiscount(100, 10)).toBe(90)
expect(calculateDiscount(100, 20)).toBe(80)
expect(calculateDiscount(50, 10)).toBe(45)
})
Python (mutmut)
安装
uv add --dev mutmut
运行 mutmut
# 运行突变测试
uv run mutmut run
# 显示结果
uv run mutmut results
# 显示特定突变体
uv run mutmut show 1
# 生成 HTML 报告
uv run mutmut html
open html/index.html
常见突变类型
// 算术运算符
// 原始: a + b → a - b, a * b, a / b
// 关系运算符
// 原始: a > b → a >= b, a < b, a <= b
// 逻辑运算符
// 原始: a && b → a || b
// 布尔字面量
// 原始: true → false
突变分数目标
| 分数 | 质量 | 行动 |
|---|---|---|
| 90%+ | 优秀 | 保持质量 |
| 80-89% | 良好 | 小改进 |
| 70-79% | 可接受 | 关注弱区域 |
| < 60% | 差 | 需要重大改进 |
改进弱测试
模式:断言不足
// 之前:突变存活
test('计算总和', () => {
expect(sum([1, 2, 3])).toBeGreaterThan(0) // 弱!
})
// 之后:突变杀死
test('正确计算总和', () => {
expect(sum([1, 2, 3])).toBe(6)
expect(sum([0, 0, 0])).toBe(0)
expect(sum([])).toBe(0)
})
模式:边界条件
// 之后:测试边界
test('验证年龄边界', () => {
expect(isValidAge(18)).toBe(true) // 最小有效
expect(isValidAge(17)).toBe(false) // 刚好低于
expect(isValidAge(100)).toBe(true) // 最大有效
expect(isValidAge(101)).toBe(false) // 刚好高于
})
最佳实践
- 从核心业务逻辑模块开始
- 在突变测试前确保80%+的覆盖率
- 增量运行(仅更改的文件)
- 首先关注重要文件
- 不要期望100%的突变分数(存在等效突变体)
工作流程
# 1. 首先确保良好覆盖率
bun test --coverage
# 目标:80%+ 覆盖率
# 2. 运行突变测试
bunx stryker run
# 3. 检查报告
open reports/mutation/html/index.html
# 4. 修复存活的突变体
# 5. 重新运行增量
bunx stryker run --incremental
# 或:npx stryker run --incremental
另请参阅
vitest-testing- 单元测试框架test-quality-analysis- 检测测试异味