ポートフォリオ → negative-control
negative-control
合格したテストは、一度失敗するところを見るまで証拠になりません。このツールはコードをわざと壊し、それを検出すべきテストを実行して、コードを元に戻します。テストが失敗すれば、そのテストは何かを守っていたことになります。合格のままなら、何も守っていなかったということで、それがはっきりします。
横にスワイプすると図の全体が見られます。
合格したテストが、守るはずの動作を一度も確認していないことがあります。実行できなかったテストも、有効な結果と誤認されかねません。
手法、判定の区分、安全のためのルールを定義しました。「見逃し」と「実行されず」を分け、作業中のファイルはコピーから復元する設計です。ツールはAIコーディングエージェントが実装しました。
弱いテストの例、3種類の判定、安全ルールの根拠となった実際の失敗から設計をたどれます。
テストは、間違った理由で合格することがある
# passes today. Also passes with the fee set to 0, # because 0 >= 0 is true. It never checked the fee. def test_small_order_has_a_fee(): assert shipping_fee(50) >= 0 # fixed: now breaking the fee turns it red def test_small_order_has_a_fee(): assert shipping_fee(50) == 8.0
テストは、違う値を確かめていたり、常に成り立つことを確かめていたり、名前に掲げたコードにそもそも届いていなかったりします。どれも見た目は、きちんと働くテストとまったく同じ「合格」です。
見分ける方法はひとつだけです。テストが守っているはずのコードを壊して、結果を見ること。人はこれを手作業でやります。1行書き換え、テストを実行し、元に戻す。失敗が起きるのは、この最後の一歩です。
壊し方を書くのはあなた。残りはツールが慎重に引き受けます。
- 壊すのは判断で、文言ではない。どのメッセージを選ぶかを変え、メッセージの文面は変えない
- コメントアウトではなく削除する。コメントアウトしたコードは、ソースを検索するテストにまだ一致してしまう
- 指定するテストは1つ。そうすれば「合格のまま」という判定が、弱いテストをまっすぐ指し示す
- レシピは古くなる。
--checkは、すべてのレシピがまだ対象のコードと一致しているかをミリ秒単位で確認する。テストスイートの中でも実行されるので、古くなったレシピは自動的に失敗として現れる
「実行されなかった」と「何も見つからなかった」は違う
検出
手を加えていないコードではテストが合格し、壊したコードでは失敗しました。やることはありません。これが目指す状態です。
見逃し
壊しても、すべてのテストが合格のままでした。テストか、テストが使うケースを直します。失敗させるためだけにレシピを変えることは、決してしません。
実行されず
置き換える文字列が見つからない、対象がプロジェクトの外を指している、壊す前からテストが失敗していた、または時間切れになった。これは「結果なし」であり、合格として読むことは決してありません。
いちばん大事なのは3つ目の判定です。長い一覧の中では、何も測れなかった実行は、何も見つからなかった実行と同じに見えます。そして人が次にするのは、そのテストを消すことです。
どの安全ルールも、実際に起きた失敗がもとになっている
初期のバージョンはgit checkoutで復元していました。これは最後のコミットの状態に戻す操作なので、同じファイルにあったコミット前のリファクタリングを消してしまいました。
出力の中から「failed」という文字を探していたときは、テスト収集時のエラーを合格と読み違え、何も確かめていない実行に「見逃し」と判定しました。
1秒以内に同じサイズの破壊を2つ続けると、2つ目も1つ目の破壊として実行され、2つ目が誤って「見逃し」と報告されました。
同じプロジェクトで2つの実行が重なると、それぞれが相手の壊したファイルを「元のファイル」として保存し、壊れたままのコードが残りました。今はプロジェクト自体にロックをかけ、2つ目の実行を拒否します。
../xのようなパスや、プロジェクト内のフォルダーのショートカットを経由すると、プロジェクト外のファイルまで壊せてしまいました。今は、対象が実際にどこを指しているかで判断します。
最初から失敗しているテストは、どう壊しても失敗するので、「検出」と数えられていました。今は各テストを、まず手を加えていないコードで一度実行します。
できていること、証明できたこと、届かないこと
できていること
- Pythonファイル1つ。pytest以外の依存なし
- レシピの形式、
--checkによる古いレシピの検出、一度に1つだけ実行するためのロック - 見つけるべき弱いテストを含んだ実例
検証済み · 2026-09-29
- ツール自身の27件のテストが合格
- ツール自身を12通りに壊し、どれもテストが失敗。ファイルはきれいに元どおり
- 実例の出力がドキュメントの記載と一致
- 公開前にレビュー。修正内容と残っている制約はREADMEに記載
範囲
- 対象は、誰かが思いついた壊し方だけ。テストが失敗しうることは示せるが、一度もテストされていない部分は示せない
- あなたのテストを実際に実行するので、サンドボックスではない。信頼できるコードに使うこと
- スキップされたテストは合格として読まれてしまい、強制終了されるとファイルを復元できない。対処法はREADMEに記載
GitHubのリポジトリと文書は英語です。