mutation-testing について
テストが不具合を検出できるかを変異テストで確認する開発者向けスキルです。TypeScript/JavaScript の Stryker と Python の mutmut を使う手順や、結果の読み方、弱いテストの改善を説明します。
主な機能
- 01変異テストの概念とスコア
- 02TypeScript と JavaScript 向け Stryker の設定
- 03Bun ネイティブランナーの指針
- 04Python mutmut のコマンド
- 05弱いテストを改善するパターン
活用シーン
- 01テストがコード変更を検出できるか確認
- 02テストスイートの弱いアサーションの特定
- 03変異テストの段階的な実行
- 04境界条件のカバレッジ改善
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ secondsky/ claude-skills --skill mutation-testingインストール後、Claude Code で/mutation-testingを入力して読み込まれたか確認します。
詳細
ミューテーションテスト
ミューテーションテストに関する専門知識です。意図的にコードを変更して、テストが実際にバグを検出できるかを検証します。
基本概念
- ミュータント:自動的に導入される小さなコード変更です
- キル:ミューテーションによってテストが失敗します(良い状態です。テストがバグを検出しています)
- サバイブ:ミューテーションがあってもテストが成功します(悪い状態です。テストが弱いことを示しています)
- スコア:キルしたミュータントの割合です(80% 以上を目指します)
TypeScript/JavaScript(Stryker)
Vitest ランナー
インストール
# Using Bun
bun add -d @stryker-mutator/core @stryker-mutator/vitest-runner
# Using 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,
}
Bun ネイティブランナー(bun test)
プロジェクトで Vitest ではなく、bun test を直接使用する場合に使います。
要件
- Bun >= 1.3.7(Bun PR #25986 で導入された TestReporter WebSocket イベントが必要です)
@stryker-mutator/core ^9.6.1
インストール
bun add -D @hughescr/stryker-bun-runner @stryker-mutator/core
設定
// stryker.conf.mjs
export default {
testRunner: 'bun',
coverageAnalysis: 'perTest',
mutate: ['src/**/*.ts', '!src/**/*.test.ts'],
thresholds: { high: 80, low: 60, break: 60 },
incremental: true,
bun: {
inspectorTimeout: 5000, // WebSocket Inspector connection timeout (ms)
// bunPath: '/path/to/bun', // only if custom Bun install
// timeout: 30000, // test timeout (ms)
// env: { DEBUG: 'true' },
// bunArgs: ['--bail'],
},
}
主な動作
- 逐次実行:テストごとのカバレッジを正確に計測するため、
--concurrency=1でテストを実行します。並列実行より遅くなりますが、テストとミュータントを正しく対応付けるために必要です。 - 並行テストのパッチ適用:ミューテーションテストの実行中に、
describe.concurrent()、test.concurrent()、it.concurrent()を自動的にパッチして逐次実行します。コードの変更は不要です。 - Inspector Protocol:Bun の WebSocket Inspector API を使用して、テストを検出し、カバレッジを対応付けます。
Stryker の実行
# Run mutation testing
bunx stryker run
# Incremental mode (only changed files)
bunx stryker run --incremental
# Specific files
bunx stryker run --mutate "src/utils/**/*.ts"
# Open HTML report
open reports/mutation/html/index.html
例:弱いテスト
// Source code
function calculateDiscount(price: number, percentage: number): number {
return price - (price * percentage / 100)
}
// ❌ WEAK: Test passes even if we mutate calculation
test('applies discount', () => {
expect(calculateDiscount(100, 10)).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)
})
Python(mutmut)
インストール
uv add --dev mutmut
mutmut の実行
# Run mutation testing
uv run mutmut run
# Show results
uv run mutmut results
# Show specific mutant
uv run mutmut show 1
# Generate HTML report
uv run mutmut html
open html/index.html
一般的なミューテーションの種類
// Arithmetic Operator
// Original: a + b → a - b, a * b, a / b
// Relational Operator
// Original: a > b → a >= b, a < b, a <= b
// Logical Operator
// Original: a && b → a || b
// Boolean Literal
// Original: true → false
ミューテーションスコアの目標
| スコア | 品質 | 対応 |
|---|---|---|
| 90%+ | 優秀 | 品質を維持します |
| 80-89% | 良好 | 小規模な改善を行います |
| 70-79% | 許容可能 | 弱い領域に注力します |
| < 60% | 不十分 | 大幅な改善が必要です |
弱いテストの改善
パターン:アサーション不足
// Before: Mutation survives
test('calculates sum', () => {
expect(sum([1, 2, 3])).toBeGreaterThan(0) // Weak!
})
// After: Mutation killed
test('calculates sum correctly', () => {
expect(sum([1, 2, 3])).toBe(6)
expect(sum([0, 0, 0])).toBe(0)
expect(sum([])).toBe(0)
})
パターン:境界条件
// After: Tests boundaries
test('validates age boundaries', () => {
expect(isValidAge(18)).toBe(true) // Min valid
expect(isValidAge(17)).toBe(false) // Just below
expect(isValidAge(100)).toBe(true) // Max valid
expect(isValidAge(101)).toBe(false) // Just above
})
ベストプラクティス
- まず、中核となるビジネスロジックのモジュールから始めます
- ミューテーションテストの前に、カバレッジを 80% 以上にします
- 段階的に実行します(変更したファイルのみを対象にします)
- 重要なファイルを優先します
- ミューテーションスコアが 100% になることは期待しないでください(同等ミュータントが存在します)
ワークフロー
# 1. Ensure good coverage first
bun test --coverage
# Target: 80%+ coverage
# 2. Run mutation testing
bunx stryker run
# 3. Check report
open reports/mutation/html/index.html
# 4. Fix survived mutants
# 5. Re-run incrementally
bunx stryker run --incremental
# or: npx stryker run --incremental
関連項目
vitest-testing- 単体テストフレームワークtest-quality-analysis- テストの問題点の検出
コメント
Markdown 対応