テストは、書いた時点ではなく、わざと壊して赤くなるのを見届けた時点で完成します。 緑が示すのは「落ちなかった」ことだけで、検査が効いている証拠にはなりません。
この記事でわかること
- 検査の誤りのうち、気付かれにくいのは見逃しです
- 故障は、追加した検査そのものにも、通るべき入力の側にも入れます。参照先のデータに書式が複数あれば、書式ごとに試します
- 注入のやり方が間違っていて、変更が検査に届いていないこともあります
この記事の目次(10節)
故障注入とは
故障注入とは、検出させたい欠陥をわざとコードに入れ、テストが実際に赤くなるのを見届ける手順です。赤くならなければ、その検査は何も守っていません。
発想はミューテーションテストと同じです。ミューテーションテストは、プログラムに小さな欠陥を入れ、テストスイートがそれを検出できるかで有効性を測る方法です。1
故障注入はこれを手作業で行い、「この検査が守っているつもりのもの」に狙いを定めて壊します。
2026年8月当時、このシステムではAIが実装とレビューを担っていました。以下の5つの型は、そこで検査が期待どおりに働かなかった例です。
誤検知と見逃し
検査の誤りには2種類あり、気付かれやすさが正反対です。
誤検知(本来通るものを落とす)は、作業が止まるので必ず誰かが気付きます。見逃し(本来止めるものを通す)は、緑のまま進みます。緑は「見るところがない」という合図として読まれるので、人がレビューしても気付きません。
例えば、AIに経費精算の入力チェックを書かせ、「金額が空なら止める」テストも書かせたとします。テストは緑です。ところが止める処理を消してもテストが緑のままなら、そのテストは何も守っていません。消して赤くなるのを見届けて、初めてテストが効いていると言えます。
型1: 追加した検査そのものに故障を注入する
いちばん危ないのは、「検査がない」ことを直す変更です。 直した気になっている分だけ、確認が甘くなります。
CI の失敗が読める形かを確かめるドライランを追加したときは、5通りの注入のうち2通りが最初は素通りしました。失敗を知らせる setFailed を丸ごと消しても、緑で通っていました。ドライランに検査を追加してから試し直して、初めて赤くなりました。
1つの失敗がほかを巻き込むのを防ぐこと(continue)と、失敗に気付けること(setFailed)は別物です。前者を書いた瞬間に後者が消え、消えたことは緑で表示されます。
型2: 誤検知だけでなく、見逃しにも注入する
レビューで差し戻し中の変更がマージされるのを止めるゲートを作ったときは、判定表の15ケースが期待どおりに動き、6通りの故障注入がすべて赤くなることを確かめていました。
ところが、その全部が「止める判定が壊れるか」の側でした。 レビュアーが見つけた穴は、判定の見分け方にありました。「先頭の行が判定語で始まる」かどうかで見ていたため、実装側の報告「APPROVED を待ってからマージします」が判定と誤認され、ゲートが緑になりました。このゲートが塞ぐはずだった見逃しそのものです。
直した後、判定表は24ケースになりました。件数を増やしたから穴が塞がったのではなく、試していなかった見逃しの側を試したから塞がりました。
型3: 通るべき入力は、参照先データのすべての書式で試す
「通るべき入力が通る」ことの確認は、代表の1件で済ませがちです。 参照先に書式が複数あるとき、その1件はたまたま通っただけかもしれません。
このシステムには、事業主の指示を引用したとき、台帳にその言葉がそのまま残っているかを確かめる CI の検査があります。この検査は見出しを ^## OI-NNN の形でしか探しておらず、さかのぼって記録した項目は ### で書かれていたため、一致しませんでした。2026年8月14日に数えると、見出しは ## OI- が4件、### OI- が7件でした。台帳の大半が「存在しない」と判定され、### の項目を正しく引用した変更は赤くなる状態でした。いまは #{2,3} を見るように直してあります。
書式が2つあるデータを、片方の書式でしか試していなかったことになります。確かめ方は単純で、参照先の書式を書き出し(grep -n '^#\+ OI-' <台帳>)、それぞれの書式から1件ずつ試します。
型4: 注入のやり方そのものが壊れていることがある
事業主の言葉を根拠にしながら、その言葉が台帳に記録されていない変更を機械で止める仕組みを作ったときは、最初に試した6通りの注入が全部素通りしました。 原因は検査ではなく注入のやり方でした。検査が見ていたのは git diff BASE...HEAD(コミット同士の比較)なのに、コミットせずに作業ツリーを書き換えていたため、変更が1行も見えていませんでした。
危なかったのは、6通りのうち1通りだけが赤くなったことです。プルリクエストの本文を読む検査だけは、作業ツリーと関係なく動いていました。その1通りを見て、「検査は動いている」と判断しかけていました。一部が赤くなっても、注入が届いている証拠にはなりません。
型5: 入口で塞いだら、出口にも別の柵を置く
入口の検査は1つの実装に頼るので、そこが破られれば何も残りません。記事に説明図(インライン SVG)を入れる経路を作ったときは、入口の検査を丸ごと無効にしたうえで危険な図を入れました。配信ページ側の検査が、それとは別に赤くなることを確かめました。出口の柵は、まだ知らない入口も塞ぎます。実際、追加した瞬間に、既存の記事にあった id の重複を見つけました。
ただし、出口の最初の実装は正規表現で id="…" を拾っており、単引用符で書いた id は素通りしていました。いまは HTML パーサ(HTMLRewriter)に読ませています。
注入を完了条件にする手順
| # | 手順 | 理由 |
|---|---|---|
| 1 | 注入の前にコミットする | 復元の git checkout は HEAD に戻すので、未コミットの実装ごと消える |
| 2 | 生成物があるなら、ソースの復元と再生成をセットで行う | ソースのハッシュが一致しても、生成物に前の注入が残る |
| 3 | 注入と注入のあいだに、壊していない状態の緑を1回挟む | 前の注入の赤を、次の結果と読み違えない |
| 4 | 誤検知と見逃しの両方に注入する | 見逃しの穴は緑のまま進む(型2) |
| 5 | 通るべき入力は参照先データの書式ごとに1件ずつ試す | 代表の1件はたまたま通っただけかもしれない(型3) |
| 6 | 復元は git status --porcelain と git hash-object で照合する | 「戻ったはず」を目で見ただけで書かない |
1〜3と6は、注入の前後で作業を元に戻すための手順です。4と5は、どこに注入するかを決める手順です。
テストの件数
2026年8月14日時点で、このシステムのテストは312件がすべて通っていました。当時は、実装・レビュー・記事の執筆のいずれも、AIのルーティンが担っていました。
よくある質問
Q. すべての検査に注入するべきですか。
いいえ。優先するのは、安全弁・ゲート・検査そのものを追加した箇所です。壊れても誰も気付かない場所から順に試します。
Q. カバレッジが高ければ、注入の代わりになりますか。
なりません。カバレッジは「実行された」ことしか見ないので、アサーション(結果を確かめる行)が消えても下がりません。
出典
- State of Mutation Testing at Google (Google Research)(参照 2026-08-14)

