概要
わざとコードを壊して、テストが落ちるかを確かめる技法(DeMillo, Lipton, Sayward, 1978)。コードを 1 箇所だけ変異させた 変異体(mutant) を作ってテストを回し、落ちれば「殺した(killed)」、通れば「生き残った(survived)」= テストにその変更を検出する力がない、と読む。
カバレッジが測るのは「実行されたか」、ミューテーションが測るのは「検査されたか」。assert が 1 つもないテストでも行カバレッジ 100% は達成できるので、その差を埋める実用的な方法が変異を当てることになる。
詳細
最小の実装
mutmut / cosmic-ray / Stryker / PIT のような専用ツールは必須ではない。手で狙いを定めた数個の変異と 30 行のスクリプトのほうが、自動生成された数千の mutant を眺めるより速く核心に届くことが多い。
- 対象ファイルの元の内容を保存する
(説明, 置換前, 置換後)の表から変異を 1 つ当て、テストを回して RED / GREEN を記録する- 各変異のあとで必ず元に戻す(
finallyやtrap ... EXIT)
置換前のアンカーには行頭のインデントまで含める。含めないと削除跡のインデントが壊れて IndentationError になり、テストが 1 件も走らないのに FAILED 行も出ないので GREEN と表示される。pytest の終了コードが 0/1 以外なら BROKEN として弾く数行が要る。測る道具も測られなければならない。
結果の読み方
| 結果 | 意味 | すること |
|---|---|---|
| RED | テストが変異を検出した | その検査には検出力がある。件数ではなく、どのテストが落ちたかを記録する |
| GREEN | 検出できなかった | 本当の穴/等価変異/到達不能な死んだコード、の 3 つに切り分ける |
| BROKEN | テストが走っていない | 変異が汚い。GREEN と混ぜない |
GREEN の 3 分類のうち、一番よく出て一番価値があるのは死んだコード。「if False: に書き換えても全テスト緑」のガードは誰も守っていない。効かない防御は次に読む人を誤解させるので、足すのではなく削る。
ミューテーションスコア = 殺した数 ÷ 変異体の総数。ベースライン(変異なし)が緑であることとセットで読む。ベースラインが赤のスコアは 77% のような「それらしい中途半端な数字」になり、バグを固定したテストが強いテストに見える。
踏みやすい罠
- assert を消すのは変異ではない — 今通っている assert を消せば必ず緑になる。壊すのは機構のほう
- 基準に動く参照を使う —
origin/mainは作業中に動く。固定 SHA を指す - 汚い変異 — 1 行消したら
NameErrorで関数ごと壊れた、は RED でもガードを測っていない。変異は意味の単位で当てる - 「網羅した」と先に書く — 分岐を列挙した表を作ってからでないと書けない
- 時計に依存する検査 — 秒解像度のタイムスタンプ衝突は、実行が秒境界をまたぐと見逃す
__pycache__— CPython は.pycの有効性をソースの mtime(秒)とサイズで判定するため、同じ文字数の置換を 1 秒以内に書くと古い.pycが使われ、変異体が生き残って見える。PYTHONDONTWRITEBYTECODE=1は書き込みを止めるだけで既存の.pycは読まれるので、__pycache__を消すことと環境変数の両方が要る
同じ問いを共有する技法
どれも「その観測は、主張が偽のときに違う結果を出すか」という一つの問いの現れ方である。
| 主張の形 | 効く検査 |
|---|---|
| 「このガード/検査は効いている」 | ミューテーション |
| 「〜は起きない」「呼ばれない」「0 件」 | ポジティブコントロール(検出できるはずの対象で先に検出を確認) |
| 「この入力範囲で正しい」 | property-based testing |
| 期待値が書けない | メタモルフィックテスティング |
| 「落ちても復旧する」 | フォールトインジェクション/カオスエンジニアリング |
| 「既存の挙動を変えていない」 | 固定 SHA との比較 + 特性テスト |
| 「性能は劣化していない」 | ベースライン付きベンチ(固定シード・相対値) |
TDD で「まず RED を見る」のも、実装が無い状態という 1 個の変異を当てているのと同じ。レビューで一番効く質問「この行、消したら何が壊れますか?」も同じ発想である。
AI エージェントに検査させるとき
「適切なテストをして」では効かない。エージェントは妥当に見える検査を選んで緑を報告でき、その報告は検査が効いていなくても同じ形で出る。渡すべきは手法名ではなく次の 2 つ。
- 主張の形 → 検査の対応表。まず指摘の主張を 1 行で書き出させ、コードの見た目から検査を選ばせない。表に当てはまらない主張は「当てはまらない」と書かせ、近い行に寄せさせない
- 止まってよい条件。「その検査は、主張が偽のときに違う結果を出すか。出ないなら証拠ではない」
運用上の要点:
- 検証エージェントの不変条件「追跡ファイルを変更しない」と、ミューテーションは正面から衝突する。「書いてよいが必ず戻せ」という条件付きの線は戻し損ねたときにしか違反が見えないので、使い捨ての複製(
git worktree add --detach)の中だけを壊す構造にする。復元は安全策ではなく変異どうしを独立させる役割に変わる - 複製には submodule も
.venvも無い。環境差で出た結果は検出力の結果と見分けがつかないので、変異の前に変異なしで 1 周回して緑を確認する(環境へのポジティブコントロール) - 「当てたか」ではなく「検査したい分岐に届いたか」を確かめさせる。分岐を列挙した表を先に作らせる
- 書き手と検査役を分け、モデルも変える(maker-checker)
- harness 自体はリポジトリに入れない。残すのは結果の表だけ。置きっぱなしのスクリプトはアンカーが腐る
ミューテーションが測っていないもの
変異体は実装から機械的に作られるので、ミューテーションテストも仕様を見ていない。測っているのは「テストが実装を今の形にピン留めしているか」であって、「実装が仕様に合っているか」ではない。実装が正しいという前提の上でテストの強度を測る道具である(テストの導出元)。
関連ページ
- テストの導出元(ホワイトボックスとブラックボックス) — 分岐カバレッジ 100% でスコアが 0% と 100% に割れた実測
- 沈黙する失敗 — 「緑」が何も語らない構造
- 作る役と確かめる役の分離(maker-checker) — 検査役を分ける理由
- pytest でカオスエンジニアリング — フォールトインジェクションの実装例
- サーキットブレーカーと指数バックオフ — 検知関数を潰してテストが赤くなるかを見る例
- AI エージェントにリファクタさせる時の完了の定義 — 交絡を潰す検証