mcpField
M
mutation-testing

mutation-testing

github.com/citypaulテスト・品質759196 回インストール4 週間前に更新
リポジトリ

mutation-testing について

本番コードにミューテーションを加え、テスト結果から有意な変更を検出できるか評価するスキルです。PR準備時のStrykerによる自動検証、ミューテーションの分類、テスト改善を重視します。

主な機能

  1. 01ミューテーション生成とテスト評価を説明
  2. 02Strykerによる自動検証を優先
  3. 03PR準備時の実行をTDDサイクルと分離
  4. 04等価なものを含め生存ミューテーションを分類
  5. 05有効な生存ミューテーションに対するテスト改善を案内

活用シーン

  1. 01変更した振る舞いをテストが保護するか確認
  2. 02高いコードカバレッジに隠れた不足を発見
  3. 03PR前にテストの有効性を評価
  4. 04生存ミューテーションに対してアサーションを強化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/citypaul/.dotfiles --skill mutation-testing

インストール後、Claude Code で/mutation-testingを入力して読み込まれたか確認します。

同梱ファイルresources/mutator-rules.md

詳細

ミューテーションテスト

優れたテスト(ファクトリー、振る舞い駆動パターン)を書くには、testing スキルを読み込んでください。このスキルでは、テストの有効性を検証することに重点を置いています。

ミューテーションテストは、**「自分のテストは本当にバグを検出できているか?」**という問いに答えます。

コードカバレッジからは、テストがどのコードを実行するかがわかります。ミューテーションテストからは、テストがコードに対する意味のある変更を検出できるかがわかります。行カバレッジが完全でも、アサーションが振る舞いを保護していることの証明にはなりません。

**基本方針:**まず自動ミューテーションハーネスを使用してください。JavaScript および TypeScript のプロジェクトでは、すでにセットアップされていない場合、Stryker を出発点として推奨してください。手動または頭の中でミューテーションを行うのは、代替手段、学習補助、または微妙な生存ミュータントに対する重点的な追加確認としてのみ使用してください。

詳細な資料はresources/ディレクトリにあります。必要に応じて読み込んでください。

リソース読み込むタイミング
mutator-rules.mdテストの計画、変更コードを調べて不足しそうな箇所を探す、手動でミューテーションを適用する、生存または同等のミュータントを解釈する場合

基本概念

ミューテーションテストのプロセス:

  1. ミュータントを生成する:本番コードに小さなバグ(ミューテーション)を導入します
  2. テストを実行する:各ミュータントに対してテストスイートを実行します
  3. 結果を評価する:テストが失敗すれば、ミュータントは「キルされた」ことになります(良い状態です)。テストが成功すれば、ミュータントは「生存した」ことになります(悪い状態です。テストがバグを見逃しています)

**重要な点:**生存したミュータントは、テストでは検出できないバグを表します。


このスキルを使うタイミング

次のような場合に、ミューテーションテスト分析を使用してください。

  • 作業の一フェーズが完了し、PR を作成できる状態になった
  • ブランチ上のコード変更をレビューする
  • TDD やリファクタリングの後に、テストの有効性を検証する
  • カバレッジがあるように見えて、実際には弱いテストを特定する
  • 不足しているエッジケースのテストを見つける
  • リファクタリングによってテストスイートが弱くなっていないことを検証する

計画および TDD との連携:

FOR EACH TDD INCREMENT:
    ├─► CONFIRM: Human approves observable acceptance criteria
    ├─► RED: Write failing test, using mutator rules to spot likely gaps
    ├─► GREEN: Make it pass
    ├─► REFACTOR: If valuable
    └─► Repeat without running the mutation harness

END-OF-PHASE PR-READINESS GATE:
    ├─► BASELINE: one full run for the accumulated branch/PR scope - the only whole-project run
    ├─► KILL MUTANTS: Strengthen tests for worthwhile survivors
    ├─► RE-RUN SCOPED ONLY: `--mutate <file>:<startLine>-<endLine>`, `--incremental`, or the diff
    │   script - never repeat the whole-project run to recheck one file, one edit, or one config fix
    └─► CLOSE: at most one final full run, present the triaged report, proceed to PR verification

自動ミューテーションハーネスは、RED-GREEN-REFACTOR の内側のループには意図的に含まれていません。テスト、作業単位、リファクタリング、コミットのたびに実行しないでください。コードベースが大きくなるにつれてコストも増加し、短いフィードバックループが次第に遅くなるためです。RED の段階では、ミューテータールールを使って、低コストで強力な例を選んでください。実装とリファクタリングのフェーズが完了し、他の点でも PR を作成できる状態になったときに、ハーネスを実行してください。

PR 準備のゲートが始まったら、通常のミューテーションプロセスに移ります。レポート全体をトリアージし、価値のある生存ミュータントに対する振る舞いテストを追加または強化し、ミュータントがキルされるか分類されるまで、対象を絞って再実行します。これらの対象を絞った再実行は同じゲート内の作業であり、作業単位ごとのミューテーションテストに戻るものではありません。


ハーネスを優先するミューテーションワークフロー

フェーズ終了時の PR 準備ゲートでは、可能な限り Stryker を使ってテストの有効性を証明してください。テストでミューテーションを検出できるかどうかを推論するだけで終わらせないでください。蓄積された変更に対してハーネスを実行し、そのレポートをもとに、対象を絞ったテストの改善を行ってください。

ステップ 1:セットアップと対象範囲を確認する
rg --files | rg '(^|/)(package.json|stryker\.config\.(mjs|cjs|js|json)|stryker\.conf\.(js|json))$'
git status --porcelain
git diff <review-base>...HEAD --name-only
  • パッケージマネージャー、テストランナー、影響を受けるパッケージ、既存の Stryker 設定を特定します。
  • 実際のレビュー範囲を使用します。単一の PR では検出したデフォルトブランチを、スタックされた PR の各レイヤーでは直下のブランチを基準にします。
  • 差分に絞ったミューテーションでは、コミット済みのブランチ変更のみが対象になります。実行前に作業ツリーがクリーンであることを確認してください。git status --porcelainが空でない場合は停止し、ステージ済み、未ステージ、未追跡の作業が対象外であることを説明してください。黙ってコミットやスタッシュをしたり、差分全体を網羅したと主張したりしてはいけません。
  • スタックされた変更の一部では、親ブランチを基準に対象のレイヤーに絞ってミューテーションを行います。最上位のブランチでは、stack-pull-requestsで求められる累積的な受け入れ確認とリポジトリのゲートも実行します。
  • モノレポでは、まず影響を受ける最小のパッケージから始め、対象を絞った実行が正常であれば、リポジトリレベルのコマンドへ範囲を広げます。
  • JS/TS プロジェクトに Stryker のセットアップがない場合は、手動でミューテーション分析を行う前に、Stryker の追加を推奨してください。
ステップ 2:Stryker がない場合にセットアップする

まず、公式ドキュメントとリポジトリのランタイム/テストランナーの制約に基づき、互換性のある Stryker の正確なパッケージバージョンを選定します。依存関係の追加も初期化処理の実行もリポジトリに変更を加えるため、ほかの依存関係やセットアップの変更と同じ承認を得てください。その後、確認済みのバージョンを指定して、リポジトリのパッケージマネージャーを使います。例:

pnpm dlx create-stryker@<reviewed-version>

次に、生成された stryker.config.* を確認し、必要に応じて調整します。

  • 利用できる場合は、プロジェクトのテストランナープラグイン(vitest、jest、mocha など)を優先します。より密接な統合が現実的でない場合に限り、汎用のコマンドランナーを使います。
  • 変更対象は、プロジェクトが保守する本番ソースコードのみとします。テスト、フィクスチャ、スナップショット、生成ファイル、宣言ファイル、ビルド出力、マイグレーション、情報量の少ないバレルファイルは除外します。
  • TypeScript の場合は、@stryker-mutator/typescript-checker を検討してください。型が無効なミュータントをテスト時間の浪費につなげず、コンパイルエラーとして報告できます。
  • 初回のセットアップでは thresholds.break の値を設定しないでください。測定済みのベースラインがない状態では、どの数値も初日からビルドを失敗させるか、見せかけの設定になります。break は未設定または null のままにし、初回実行のスコアを報告してから、そのスコアに基づいてしきい値を提案してください。
  • セットアップの変更をレビュー可能な状態に保ちます。依存関係、設定、スクリプトに加えて、Stryker をリポジトリに新規追加する場合は、実行時に未追跡の成果物が残らないよう、一時出力とレポート出力(.stryker-tmp/ と、設定したレポートディレクトリ。通常は reports/)を対象とする .gitignore の項目も追加します。
ステップ 3:役立つコマンドを提案する

リクエストで指定されているのが 1 つか 2 つだけの場合も、プロジェクト全体用、キャッシュ用(--incremental)、ブランチ差分用の 3 つのスクリプトをすべて追加します。キャッシュ用スクリプトがあると、ゲートの対象を絞った再実行を低コストで行えます。プロジェクトやリクエストですでに示されている命名規則に合わせてください。

{
  "scripts": {
    "mutation": "stryker run",
    "mutation:incremental": "stryker run --incremental",
    "mutation:diff": "node scripts/stryker-diff.mjs <review-base>"
  }
}

mutation:diff ヘルパーは、次のようにします。

  • ベースブランチ引数を読み取り、デフォルトではリポジトリで検出したデフォルトブランチを使います。
  • git status --porcelain が空でない場合は、<base>...HEAD にはステージ済み、未ステージ、未追跡の作業が含まれないことを説明して、実行を拒否します。
  • git diff --name-only -z --diff-filter=ACMRTUXB <base>...HEAD で変更ファイルを収集し、NUL 区切りのレコードとして解析します。ファイル名には空白や改行が含まれることがあります。
  • プロジェクトのソース拡張子に一致する変更済みの本番ファイルのみを対象にします。
  • テスト/spec ファイル、フィクスチャ、スナップショット、生成ファイル、宣言ファイル、ビルド出力は除外します。
  • シェルを介さず、リポジトリローカルの Stryker バイナリを呼び出します。--mutate オプションでカンマ区切りのパターンが必要な場合は、カンマを含むファイル名を結合前に明確なメッセージとともに拒否します。対象範囲を黙って変更してはいけません。
  • 変更対象の本番ファイルがない場合は、明確に終了します。

package.json 内に複雑なシェルを詰め込まず、小さな Node ヘルパーを使います。コマンド置換では NUL 区切りのパスを保持できず、*、!、カンマ、改行の引用符処理はシェルによって不安定になるためです。

レポートで特定のサバイバーが示された場合は、範囲の狭い追加チェックに正確な行範囲を指定します。

pnpm exec stryker run --incremental --force --mutate src/example.ts:42-57

リポジトリで実際に使われているパッケージマネージャーを使用します。例ではリポジトリローカルのバイナリを示すために pnpm を使っています。実行コマンドによって、頻繁に変わる Stryker のリリースが黙ってダウンロードされないようにしてください。

ステップ 4:実行とトリアージ

PR の準備状況を確認する段階では、ブランチに関するフィードバックを得るため、まず mutation:diff を実行します。プロジェクト全体に対する mutation の実行が妥当なのは、Stryker の導入、共有テストインフラの変更、CI ゲートの準備、または広範なテスト強化の検証を行う場合です。このゲートで実行できるのは最大 2 回です。テストを変更する前にベースラインを 1 回、変更後の検証を 1 回実行します。それ以外の実行はすべて対象を絞ります(--mutate <file>[:<start>-<end>]、--incremental、または diff スクリプト)。設定の編集を再確認する場合や、強化したばかりの単一ファイルを確認する場合も同様です。

ベースラインを実行する前に、テストハーネスが動作することを確認します。Stryker の設定を書いた後、または変更した後の最初の実行では、小さなファイル 1 つを対象にしたスモークテストを行います(--mutate <file>[:<start>-<end>])。プロジェクト全体を実行してはいけません。数秒で完了し、変異が測定される前に実行を中断させる問題(ランナープラグインの不足、サンドボックスのコピーから読み取れないパス、ポートや権限によるブロック)を検出できます。結果ではなく試行回数を数えてください。クラッシュした、中断された、または変異レポートが出力されなかった実行も、2 回のうち 1 回を消費します。そのような中断が起きたら、原因を修正し、対象を絞ったスモークテストで再確認してから、プロジェクト全体の実行を行います。

ハーネスの出力を読み直すために、ハーネスを再実行してはいけません。ベースライン実行では、すでに生存した変異の一覧が出力されています。一度だけ取得し(... 2>&1 | tee mutation-baseline.log を実行するか、json レポーターを追加してそのファイルを読み込みます)、コピーを使ってトリアージしてください。生存した変異を検索するためにプロジェクト全体を再実行すると、全体実行を 1 回消費するうえ、最初の実行から分かること以上の情報は得られません。

ゲートを閉じる前に、生存した変異をすべて分類し、その分類を提出するレポートにも記載してください。続行するかどうかを尋ねるために中断する場合も同様です。生存した変異を 1 つずつ取り上げ、まず最初に等価性を確認します。この変異によって異なる値が返される、異なる状態が永続化される、または異なるメッセージが出力される入力はあるでしょうか。なければ、それは Equivalent です。削除しても同じ結果になるガード、+ 0 や * 1 のような恒等演算、冗長な早期リターン、到達不能な分岐などが該当し、テストで失敗させることはできません。その理由を 1 行で記録してください。この確認を経た生存変異だけが、失敗させるか、保留にする候補になります。したがって、生存した変異はそれぞれ、新しいテストで失敗した、理由を添えて Equivalent と判定した、または判断が必要な項目として明示的に保留した、のいずれか 1 つに必ず分類してください。まだ何も失敗させておらず、報告のみを行う場合でも、等価性の確認を実施してください。生存した変異をすべて失敗させると申し出ることは、それらがどれも等価ではないと主張することになります。この主張は省略によって示すのではなく、変異ごとに確認する必要があります。分類せずに生存した変異を列挙しただけでは、トリアージ済みのレポートとは言えません。また、生存した変異をすべて欠陥として扱うのも適切ではありません。

Stryker の検出結果を分類します。

カテゴリ説明必要な対応
Killed変異を適用するとテストが失敗した不要 - テストは有効です
Survived変異が有効な状態でもテストが成功した等価でない限り、テストを追加または強化します
No Coverageこのコードを実行するテストがない動作を確認するテストを追加します
Equivalent変異によって同じ動作が生じる不要 - 実際のバグではありません

明らかな問題はすぐに修正します。

  • 境界値テストの不足
  • アサーションが弱い、またはない
  • 分岐の片側しかカバーしていない
  • 副作用の検証がない
  • 金銭、権限、適格性、安全性、データ損失など、重要度の高いビジネスルール

人の判断が必要な微妙な生存変異については、利用可能であれば、現在のハーネスに備わっている構造化された質問機能を使用します。それ以外の場合は、簡潔な質問をプレーンテキストで 1 つ尋ねます。具体的な選択肢を示し、変異の内容とトレードオフを説明してください。動作が意図的に未定義の場合、正しいドメインルールが不明な場合、テストにコストがかかる、または不安定になる場合、あるいは変異が等価かもしれないものの確信がない場合に、この方法を使います。

ステップ 5:TDD でサバイバーを倒す

倒す価値のあるサバイバーごとに、次の手順を実施します。

  1. ミュータントを残すか、再作成します。
  2. 適切な理由でミュータントに対して失敗する、最小限の動作テストを書きます。
  3. 元の本番コードに戻します。
  4. 新しいテストが成功することを確認します。
  5. 変更されたファイルまたは行の範囲に絞って Stryker を再実行し、その後、差分コマンドを再実行します。

実装の詳細に過剰適合したテストは避けてください。優れたミューテーションテストでは、戻り値、永続化された状態、発行されたイベント、権限、メッセージ、意味のある協調オブジェクトの呼び出しなど、観測可能な動作を検証します。

Stryker の設定ガイダンス

Stryker は、JS/TS のミューテーションテストで通常使用するエントリーポイントにしてください。

初期設定

stryker.config.mjs または初期化ツールが生成する形式を使用してください。Vitest ランナーを使用するには、@stryker-mutator/core とともに @stryker-mutator/vitest-runner をインストールする必要があります。一般的な初期設定は次のとおりです。

export default {
  testRunner: "vitest",
  coverageAnalysis: "perTest",
  reporters: ["html", "clear-text", "progress"],
  mutate: [
    "src/**/*.{ts,tsx,js,jsx}",
    "!src/**/*.test.{ts,tsx,js,jsx}",
    "!src/**/*.spec.{ts,tsx,js,jsx}",
    "!src/**/*.d.ts"
  ]
}

プロジェクトに合わせて testRunner、mutate、vitest.configFile、ビルドコマンド、チェッカープラグインを調整してください。レイアウトの異なるリポジトリに、この設定をそのまま機械的に適用しないでください。

Vitest Browser Mode に関する注意点: Stryker の Vitest ランナーが対象とするのは、Node ベースのテストプロジェクトであり、ブラウザモードのテストプロジェクトではありません。テスト対象の主張に実際のブラウザーの動作が必要で、そのサポート状況とコストが適切なためにリポジトリで Browser Mode を使用している場合は、mutate を Node テストでカバーされる UI 以外のソースに絞るか、複数プロジェクトの Vitest 構成に含まれる Node プロジェクトを Stryker に指定してください。UI パッケージをミューテーションテストできると決めつける前に、Stryker のドキュメントで Browser Mode の現在のサポート状況を確認してください。

CI と品質ゲート
  • 既存のテストスイートにサバイバーが多い場合は、レポートのみ、または差分のみのミューテーションチェックから始めます。
  • 現実的なベースラインを確立した後にのみ、失敗しきい値を追加します。
  • HTML レポートとプレーンテキストレポートを CI アーティファクトとして保存します。
  • ローカルでのフィードバックを速めるためにインクリメンタルモードを使用しますが、古くなった前提に頼らないよう、定期的に全実行を強制します。
  • ミューテーションスコアは指標として扱い、見栄えのための数値にはしないでください。変更されたコードやリスクの高いコードにある、テストをすり抜けたミュータントやカバレッジのないミュータントを優先します。
手動ミューテーションによる代替手段

Stryker が使用できない場合、またはレビュー対象のコードを指定できない場合は、resources/mutator-rules.md を読み込み、該当するオペレーターを手動で適用します。次のミューテーションに進む前に、必ず各ミューテーションを元に戻してください。手動ミューテーションでも同じサイクルに従います。ミューテーションを適用し、テストを実行し、分類し、明らかな不足を修正し、判断が分かれる点について相談し、結果を報告します。


まとめ:ミューテーションテストの考え方

すべてのコード行で重要な問い:

「ここにバグを入れたら、テストで検出できるだろうか?」

各テストで、次の変更を検出できることを確認します。

  • 算術演算子の変更
  • 境界条件の変更
  • ブール論理の反転
  • 文の削除
  • 戻り値の変更

覚えておきましょう:

  • カバレッジが測るのは実行であり、ミューテーションテストが測るのは検出です
  • アサーションのないテストではミュータントを倒せません
  • 境界値、ブール値の混在ケース、恒等値以外の値、観測可能な副作用によって、多くの一般的なミュータントを倒せます
  • ミューテーターのチェックリストと例の全体については、resources/mutator-rules.md を読み込みます

コメント