mcpField
M
mutation-testing

mutation-testing

github.com/laurigatesテスト・品質5966 回インストール今日更新
リポジトリ

mutation-testing について

mutation-testing は、意図的なコード変更を加えてテストがバグを検出できるか評価するスキルです。TypeScript/JavaScript 向けの Stryker と Python 向けの mutmut を扱い、生き残ったミュータントの解釈や自作ハーネスの誤判定要因を説明します。

主な機能

  1. 01ミュータントの検出・生存を解説
  2. 02Stryker の設定と実行を案内
  3. 03mutmut の設定と実行を案内
  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 は、ある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は、まさにそのコントロールです)

参考資料

コメント