mutation-testing について
mutation-testing は、意図的なコード変更を加えてテストがバグを検出できるか評価するスキルです。TypeScript/JavaScript 向けの Stryker と Python 向けの mutmut を扱い、生き残ったミュータントの解釈や自作ハーネスの誤判定要因を説明します。
主な機能
- 01ミュータントの検出・生存を解説
- 02Stryker の設定と実行を案内
- 03mutmut の設定と実行を案内
- 04ミューテーションスコアと生存結果の解釈
- 05自作ハーネスの落とし穴を解説
活用シーン
- 01動作を検証できていないテストの特定
- 02不足するアサーションや境界条件の発見
- 03テストスイートで生き残ったミュータントの調査
- 04独自ハーネスによる変異テスト結果の評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ laurigates/ claude-plugins --skill mutation-testingインストール後、Claude Code で/mutation-testingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
REFERENCE.mdreferences/hand-rolled-harness-failures.mdtriaging-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 は、ある1つのコードベースで得られた経験則であり、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 より少なく報告します
上記はすべてフレームワークを前提としています。実際のミューテーションテストでは、独自に作成したループを使うことも多くあります。たとえば、変異を適用し、1つのアサーションを実行し、失敗を捕捉します。これは通常、ユニットテストスイートではなく、ジェネレーターやビルダーのビルド時チェックがテスト対象であるためです。
このループを書く価値はあります。しかし、フレームワークなら無料で提供される記録機能が1つ欠けています。**Stryker と mutmut は、各ミュータントをどのテストがキルしたかを教えてくれます。**独自のハーネスでは通常、何かが失敗したことだけが報告され、「何かが失敗した」は「テスト対象のチェックが失敗した」と区別できません。これが原因で起きる問題は4つあります。最初の3つは1回のセッションで、4つ目は別のセッションで確認されています。
独自に作成したハーネスの表を信頼する前に、references/hand-rolled-harness-failures.md を確認してください。§1:先行するチェックがテスト対象のチェックを遮る。§2:対象のチェックだけが検出できる変異を選ぶ。§3:テーブルの変異後も、インポート時に派生した状態が古いまま残る。§4:変異させたファイルがインポートされていなかった(誤った MISSED。ランタイムごとの検索パス表を参照)。
緑のテーブルが意味すること
すべての変異に対してCAUGHTを出力するハーネスは、テストスイートが健全である証拠としてよく引用されます。しかし、それが証明するのは、より弱いことです。
すべてCAUGHTのテーブルが証明するのは、各変異が何らかのアサーションによって検出されたということです。 意図したアサーションが検出したとは限りません。また、個々のアサーションが失敗し得ることも証明しません。
手軽な追加を2つ行えば、このギャップの大部分を埋められます。
- ハーネスが見逃すべき、意図的なノーオペレーション変異。すべてCAUGHTのテーブルは、壊れたハーネスと見分けがつきません。MISSが1つ想定されていれば、両者を区別できます。この対称性は逆の場合にも当てはまり、コントロールでは解消できません。すべてMISSEDのテーブルも、壊れたハーネスと同じように見えます。また、設計どおりにMISSEDを報告するノーオペレーションは、実際の変異が読み込まれずにMISSEDを報告する状態と見分けがつきません。まずMETA/accept-all行を確認してください。ハードコードされた
raiseやaccept-allによってテストスイートが失敗状態にならないなら、それはアサーションが弱いのではなく、編集したファイルをハーネスが実行していない証拠です(§4)。 - 例外だけでなく、メッセージを検証してください。変異と失敗メッセージの想定部分文字列を照合することで、結果がマスクされた場合に、黙って合格するのではなくハーネスの失敗として検出できます。
変異スコアの目標
| スコア | 品質 | 対応 |
|---|---|---|
| 90%+ | 優秀 | 品質を維持する |
| 80-89% | 良好 | 小規模な改善を行う |
| 70-79% | 許容可能 | 弱点に重点的に取り組む |
| 60-69% | 要改善 | 不足しているテストを追加する |
| < 60% | 不良 | 大幅なテスト改善が必要 |
エージェント型の最適化
| コンテキスト | コマンド |
|---|---|
| 高速な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は、まさにそのコントロールです)
参考資料
- Stryker:https://stryker-mutator.io/
- mutmut:https://github.com/boxed/mutmut
- 変異テストの概要:https://en.wikipedia.org/wiki/Mutation_testing