Claude Media
Claude CodeでStrykerのミューテーションテストを回し弱いテストを補強する

Claude CodeでStrykerのミューテーションテストを回し弱いテストを補強する

Strykerの生存ミュータントをClaude Codeに渡してテストを補強させ、mutation scoreの下限をStop hookで完了条件にする手順です。設定例とCLAUDE.mdの書き方を載せます。

Claude Codeに「テストを書いて」と頼むと、行カバレッジは上がります。ただしそのテストが、コードの間違いを本当に検出できるかは別の話です。StrykerJSのミューテーションテストは、本番コードに小さな改変(ミュータント)を入れて、テストが落ちるかどうかを見ます。落ちなかった改変が「生存ミュータント」で、テストの穴の場所をそのまま示します。この一覧をClaude Codeに渡し、mutation scoreの下限を完了条件にすれば、弱いテストの補強を自動で回せます。

生存ミュータントは「テストの穴」の一覧になる

Strykerはテストを実行し、各ミュータントについて結果を出します。テストが失敗して改変を検出できればKilled、検出できなければSurvivedです。mutation scoreは、この検出できた割合を指します。

行カバレッジは「その行が実行されたか」しか見ません。アサーションが甘いテストでも、行を通れば100%になります。ミューテーションテストは、実行された行の結果をテストが本当に確かめているかを測る点が違います。

Strykerは、カバレッジ分析を有効にするとSurvivedとNoCoverageを区別します。

  • Survived: ミュータントの箇所をテストが実行したのに、失敗しなかった
  • NoCoverage: どのテストも、その箇所を実行していない

前者は既存テストのアサーションを強める話、後者は新しいテストを足す話になります。Claudeへの指示も分けたほうが、意図に沿った修正が返ってきます。

Strykerを設定する

セットアップは公式のとおり、次のコマンドで始めます。初期化の質問に答えるとstryker.config.mjsが作られます。

npm init stryker@latest
npx stryker run

以降はVitestを使うプロジェクトを例にします。VitestランナーはStryker v7.0から使えるプラグインで、@stryker-mutator/vitest-runnerを入れてtestRunnerにvitestを指定します。

{
  "testRunner": "vitest",
  "mutate": ["src/**/*.ts", "!src/**/*.spec.ts"],
  "reporters": ["clear-text", "json"],
  "coverageAnalysis": "perTest",
  "incremental": true,
  "thresholds": { "high": 80, "low": 60, "break": 60 }
}

各キーの役割は次のとおりです。

キー効果既定値
mutate効果改変する本番コードの範囲。テストファイルは含めない既定値src・lib配下のjs系ファイル(テストを除く)
reporters効果結果の出力先既定値clear-text, progress, html
incremental効果前回結果を再利用し、変更箇所だけ再実行する既定値false
thresholds.break効果mutation scoreがこれを下回ると終了コード1既定値null(ビルドを落とさない)

thresholdsは3つの値をすべて指定する形式で、1つだけの指定はできません。breakの既定はnullなので、何も書かなければscoreが低くても失敗しません。完了条件に使うには、ここに数値を入れることが前提になります。

clear-textレポーターは、既定でミュータントの一覧とスコア表を出力します。jsonレポーターの出力先は既定でreports/mutation/mutation.jsonです。Claudeに読ませる素材として、標準出力の一覧か、このJSONのどちらかを使えます。

生存ミュータントをClaudeに渡して補強させる

全ファイルを一度に改変すると時間がかかります。改変対象は、--mutateで1ファイルか1ディレクトリに絞って渡します。

npx stryker run --mutate src/pricing/discount.ts

出力にSurvivedのミュータントが残ったら、そのままClaude Codeに貼ります。例えば次のように指示します。

src/pricing/discount.ts の Stryker 結果を貼ります。
Survived を 1 件ずつ見て、次の順で対応してください。
1. そのミュータントで挙動が変わる入力を特定する
2. 既存テストに足りないアサーションか、テストケースを追加する
3. npx stryker run --mutate src/pricing/discount.ts を再実行して
   Killed に変わったことを確認する
本番コード(discount.ts)は変更しないでください。

最後の一文が要点です。生存ミュータントを消す近道は、本番コードを書き換えてミュータント自体をなくすことです。テストの補強が目的なら、変更してよい範囲を明示しておきます。

再実行の待ち時間は、incrementalが縮めてくれます。Killedだった結果は、そのミュータントを倒したテストが残って変更もなければ再利用されます。Killedでないミュータントも、新しいテストがそれを覆わず、テストが変更されていなければ再利用対象です。逆に言えば、Claudeが足したテストで判定が変わるミュータントは、再実行の対象に入ります。

Claudeに渡す出力を絞る

出力が長いと、貼る量が増えて文脈を圧迫します。clearTextReporterのオプションで、渡す内容を絞れます。

{
  "clearTextReporter": {
    "reportTests": false,
    "logTests": false,
    "reportScoreTable": true,
    "skipFull": true
  }
}
  • reportTestsとlogTestsをfalseにすると、実行したテストの一覧が出力から外れます。生存ミュータントの修正に、全テストの列挙は要りません
  • skipFullをtrueにすると、スコア表からmutation score 100%のファイルが消えます。要対応のファイルだけが残ります
  • reportMutantsは既定でtrueです。生存ミュータントの一覧が必要なので、切らないでください

TypeScriptのプロジェクトでは、disableTypeChecksも知っておくと役立ちます。Strykerは改変を入れる際に型エラーを作ってしまうため、型チェックを無効にしたうえで動きます。v7.0以降の既定はtrueで、対象ファイルの先頭に// @ts-nocheckが挿入されます。型エラーを検出したい場合の設定は別にあるので、まず既定のまま動かして生存ミュータントの一覧を得るのが出発点です。

CLAUDE.mdに補強のルールを書く

毎回プロンプトを書く代わりに、ルールをCLAUDE.mdへ置きます。公式は、CLAUDE.mdを1ファイル200行未満に保つことと、検証できる具体的な書き方を勧めています。また、特定の場所でだけ効く指示はパス指定のルールに分ける構成が案内されています。ミューテーション関連の記述は、src配下の作業でだけ読まれれば足ります。

次は.claude/rules/mutation-testing.mdの例です。中身は公式のパス指定ルールの書式に沿った、この記事での一例です。

---
paths:
  - "src/**/*.ts"
---
 
# ミューテーションテスト
 
- 対象ファイルを変更したら
  `npx stryker run --mutate <変更したファイル>` を実行する
- Survived のミュータントは、テスト側にアサーションかケースを足して倒す
- 本番コードを、ミュータントを消す目的で変更しない
- `// Stryker disable` コメントは、人の承認なしに追加しない
- mutation score は 60 以上を保つ(設定の thresholds.break と同じ値)

「60以上」のように数値を書くと、Claudeが守れているかを確認できます。「テストを十分に強くする」のような書き方だと、公式が指摘する曖昧さの問題に当たります。

ただし、CLAUDE.mdは強制ではありません。公式は、CLAUDE.mdをシステムプロンプトではなくユーザーメッセージとして渡される文脈と説明し、厳密な遵守は保証されないとしています。特に「必ずこの時点で実行する」種類の指示は、フックにするよう案内されています。

Stop hookでmutation scoreを完了条件にする

Stop hookは、Claudeが応答を終えるたびに発火します。ここでStrykerを走らせ、thresholds.breakを下回っていたら差し戻す構成にします。Strykerはscoreがbreak未満なら終了コード1で終わるので、コマンドの成否がそのまま判定になります。

.claude/settings.jsonには次のように登録します。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/check-mutation-score.sh",
            "timeout": 900
          }
        ]
      }
    ]
  }
}

スクリプトは、公式が示すStopフックの型に従います。標準入力のJSONからstop_hook_activeを読み、trueならすぐ終了します。差し戻しは、トップレベルのdecision: "block"とreasonを返す形式です。reasonはClaudeに戻され、作業が続きます。

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
 
if ! OUT=$(npx stryker run --incremental 2>&1); then
  jq -n --arg r "mutation score が下限未満です。Survived を補強してください。$(echo "$OUT" | tail -n 40)" \
    '{decision: "block", reason: $r}'
fi
exit 0

--mutateを付けていないので、設定ファイルのmutate範囲すべてが対象です。範囲が広いプロジェクトでは、変更ファイルだけを渡す運用にしたほうが現実的です。

このスクリプトには考慮点が2つあります。

  • stop_hook_activeで抜ける形は、差し戻しが1回で終わります。補強が1往復で終わらないなら、この判定を外して回数を伸ばせます。ただし公式によれば、Claude Codeは進捗なしにStopフックが8回連続でブロックした場合、フックを上書きして止めます
  • Stopフックは、ユーザーが中断したときには発火しません。完了条件の最後の砦とは言い切れないため、CIでも同じstryker runを実行しておくと二重に守れます

テスト実行をフックに載せる基本形は、Claude Codeでテストを自動実行するhooksの記事にあります。PostToolUseとStopのどちらに置くかの判断もそこで扱っています。Strykerは1回の実行が長いので、PostToolUseではなくStopに置く構成が向いています。

つまずきやすい点

テストだけを通すコードを書かせない

Survivedを倒す目的で、Claudeが特定の入力にだけ反応する分岐を足す余地は残ります。指示に「一般式で書く」と明記するのが基本で、詳しくはClaudeがテストだけ通すハードコードを防ぐプロンプトの書き方にまとめています。

Vitestのrelatedが効かない構成がある

Vitestランナーは、既定で改変ファイルに関連するテストだけを走らせます。テストが本番コードを直接importせず、HTTP経由でサーバーを呼ぶ統合テストでは、関連が見つかりません。その場合はvitest.relatedをfalseにします。Vitestのブラウザモードは、このランナーで未対応です。

incrementalが見ない変更がある

incrementalが検出するのは、改変対象ファイルとテストファイルの変更です。それ以外のファイル、更新した依存、環境変数、.snapファイルの変更は検出されません。設定や依存を大きく変えたら、--incrementalを外して全件を回し直します。

commandランナーは遅い

テストランナー用のプラグインがない環境では、npm testを実行するcommandランナーが既定です。カバレッジ分析ができないため、すべてのミュータントで全テストを走らせることになります。

タイムアウトで落ちるミュータントがある

改変が無限ループを生むことがあるため、Strykerはテスト実行に時間制限を設けます。上限はnetTimeMs * timeoutFactor + timeoutMS + overheadMsで計算され、timeoutMSの既定は5000です。負荷の高いマシンで誤判定が出るなら、timeoutMSを延ばします。

改変対象にテストファイルを入れない

mutateに指定するのは本番コードで、テストではありません。Vitestのin-source testingを使う場合は、テストが同じファイルに入るため、// Stryker disable allのコメントでテスト部分を除外する必要があります。

使い分けの早見表

場面向いているやり方
1ファイルを改修中向いているやり方--mutate <file> を手動で実行し、結果を貼る
反復して補強を回したい向いているやり方incremental: true とCLAUDE.mdのルール
完了条件として強制したい向いているやり方Stop hook + thresholds.break
プルリクエストごとの品質ゲート向いているやり方CIで同じ stryker run

行カバレッジを埋めるところまでは、Claude Codeでテストを書かせる実践手順の流れが使えます。その後にStrykerを重ねると、「書いたテストが実際に効いているか」まで確かめられます。

まとめ

Strykerの生存ミュータントは、テストの穴を具体的な差分で示します。この一覧をClaude Codeに渡せば、補強は「何をどう足すか」がはっきりした作業になります。手順は、thresholds.breakを設定し、CLAUDE.mdかルールに補強の方針を書き、Stop hookでscoreの下限を強制する、の3段です。本番コードを書き換えない指示と、incrementalによる再実行の短縮が、運用を続けやすくする要点になります。

この記事を共有:XはてブLinkedIn