定義
ミューテーションテスト(mutation testing) は、プログラムに欠陥を一つずつ人為的に埋め込んだ変異体(mutant)を作り、既存のテストがそれを失敗として捕まえられるかを測る手法である。測定対象はプロダクトコードではなく、テストの欠陥検出力である。
なぜ必要か
カバレッジは「テストがその行を通ったか」だけを数える。通ったが何も確認していない行も 100% として計上される。
it("注文を作成する", async () => {
await createOrder({ items });
// assertion なし — カバレッジは上がり検証はゼロ
});
ミューテーションテストは問いを反転させる。「テストがコードを実行したか」ではなく、「コードが誤っているときテストが赤くなるか」 を問う。> を >= に、&& を || に、戻り値を null に変えても全部緑のままなら、そのテストはその地点を守っていないという直接的な証拠になる。
とくに「とにかく通そう」という圧力の中で assertion が少しずつ緩められたテストを見つけるのに強い。カバレッジのように行数で誤魔化すことができない。
仕組み
- 元のコードに mutation operator を適用し、欠陥が一つだけの変異体を大量に生成する。
- 各変異体に対して関連テストを実行する。
- テストが失敗すれば killed、すべて通れば survived として記録する。
mutation score = killed / (全体 − equivalent)を計算する。
代表的な mutation operator は次のとおり。
| 演算子 | 変異の例 | 生き残った場合に疑うこと |
|---|---|---|
| 条件境界 | a > b → a >= b | 境界値テストの不在 |
| 論理演算子 | && → || | 複合条件の一部分岐が未検証 |
| 戻り値置換 | return x → return null | 戻り値を誰も確認していない |
| 呼び出し削除 | logAudit() を削除 | 副作用を検証していない |
| 定数変更 | 0 → 1 | 初期値・オフセットの検証不足 |
equivalent mutant はコードを変えても観測可能な挙動が原本と完全に同じで、原理上決して殺せない変異体である。自動判別は不可能(停止性問題と等価)なため、人が理由を書いて除外する必要がある。
実務での適用
多くのツールは変更分限定の実行に対応している。全体実行は非常に遅いため、この選択肢が事実上必須である。
# JS/TS: Stryker
npx stryker run --incremental --mutate "src/domain/**/*.ts"
生き残った変異体が出たら、その地点を守るテストを追加したうえで、欠陥を元に戻して再び緑になるかを確認する。
// survived: `total > limit` → `total >= limit` を誰も捕まえられなかった
it("限度額とちょうど同じ金額は通す", () => {
expect(isOverLimit(1000, 1000)).toBe(false);
});
it("限度額を超えたら止める", () => {
expect(isOverLimit(1001, 1000)).toBe(true);
});
CI には変更ファイル対象でのみ組み込み、全体実行は夜間ジョブに分離する構成が無難である。
トレードオフ
最大のコストは実行時間である。変異体数 × テスト時間なので、単純計算でもテストスイートの数十〜数百倍になる。並列化、変更分限定、変異体を通過するテストのみを選ぶ最適化で削減できるが、依然として重い。
二つ目のコストは equivalent mutant を判別する人の時間である。これがあるためスコアは決して 100% に達せず、達成を目標に据えるとチームが無意味な作業を始める。
得られるのは他の指標が与えない情報だ。カバレッジは「どこを見ていないか」を示すが、ミューテーションテストは**「見てはいるが捕まえられない場所はどこか」** を行単位で示す。
使うべきでない場合
- テストがまだ不安定(flaky)なコードベース。 失敗が欠陥によるものか揺れによるものか区別できず、結果全体が無意味になる。安定化が先。
- 判定基準が整理されていない状態。 誤った期待値を基準に変異体を殺すと、誤った挙動をより強固に固定するだけである。test-oracle が前提条件になる。
- UI レンダリング、ログ形式、実験的コードなど変更が多く正確性のコストが低い領域。実行コストに見合わない。
- スコアを KPI として強制したい組織。 数字を埋めるために無意味なテストを増やせば、カバレッジのときと同じ歪みが繰り返される。
よくある間違い
- コードベース全体に一度に走らせる。 結果が出るまで数時間かかり、誰も見なくなる。リスクの高い境界から狭く始める。
- 変異体を殺した後の復元確認を飛ばす。 正常なコードでも失敗するテストを新たに入れてしまう事故がここで起きる。
- 生き残った変異体をすべて equivalent と決めつける。 根拠を書かないと、次の人が同じ判断を最初から繰り返す。
- mutation score をカバレッジの代替指標としてのみ使う。 有用な成果物はスコアではなく生き残った変異体の一覧であり、その一覧こそがテスト空白の座標である。
関連概念
- test-oracle — 何が正解かという問題。ミューテーションテストはその判定が実際に機能するかを検証する
- react-render-count-isolation-testing — assertion を具体的にして検出力を上げる別の事例