AIはいまや、コードだけでなくそのテストも一緒に書いてくれます。「実装して、テストも付けて、緑になるまで直して」——この一言で、動いてテストも通るコードが返ってくる。便利です。ただ、ここで一度立ち止まる価値があります。テストが通っていれば安心、とつい受け取ってしまうけれど——実際どうなの? 返ってきた「緑」は、コードが正しいことの証明なのか。それがこの記事の出発点です。
前に、AIで作ったツールを半年後に誰がどう保守するか、という話を書きました(AIで作ったコード、半年後に誰がメンテするのか)。「自分で運用する」と決めたなら、次に必ず来るのが品質の担保です。そして日々の保守で品質を担保する主役は、たいていテストです。テストが緑なら直してよし、赤なら直す。この単純なルールが回っている限り、テストの信頼性がそのまま運用の信頼性になります。
問題は、AIが書いたテストの「緑」は、思っているより簡単に嘘をつくということです。この記事では、緑をどこまで信じてよいかを見分ける観点と、私たちが自動開発ループで実際に「緑の嘘」を塞いだ設計を紹介します。
この記事で学べること
- 「テストが赤から緑になった」が、なぜコードの正しさを証明しないのか
- 緑への圧力がかかったAIがテストを弱めにいく構造と、その多層ガード
- 「指摘が消えた=直った」と解釈する自動判定の危うさ
- 一度も赤くならないテストを疑うべき理由
- あなたのプロジェクトでAIのテストを信用する前に見る、実践的な観点
「緑」が証明しているのは、あなたが思うより少ない
まず、テストが赤から緑に変わったとき、それが実際に保証していることを正確に言語化してみます。
赤から緑への変化が実証するのは、テストが今の実装を制約しているということだけです。これは「テストが正しい挙動を検証している」こととは、はっきり別物です。極端な話、結果が存在すること だけを確かめる assert——中身が正しいかは一切見ない——でも、実装が動きさえすれば赤から緑になります。
AIにテストごと書かせると、この隙間が広がります。実装とテストを同じ文脈で生成するので、実装の癖に合わせてテストが書かれる。実装が返す形をそのままなぞる assert になりやすく、「本来こうあるべき」という外側の基準は入りにくい。結果、実装とテストが仲良く同じ勘違いをしていても、テストは元気に緑を出します(実装とテストが口裏を合わせている状態、と言ってもいい。正直、いちばん見抜きにくいやつです)。
だから、緑を見たときに最初に確かめるのは「何本通ったか」ではなく、そのテストは、実装が間違っていたらちゃんと赤くなるのかです。落ちない前提のテストは、通っていても情報量がゼロに近い。
整理すると、「赤→緑」が言えることと言えないことは、はっきり分かれます。
| 「赤→緑」が証明すること | 証明しないこと |
|---|---|
| テストが今の実装を制約している | テストが正しい挙動を検証している |
| 実装が動いて例外を出していない | assert が値・状態の中身まで見ている |
| いまの実装に対してテストが通る | 実装が間違ったらテストが赤くなる |
AIは、緑への最短経路を通る
ここからは、その隙間が自動化ループの中でどう暴発するかの実例です。
私たちは、Issue を渡すと分析・計画・実装・テスト・評価・PR 作成まで自動で進む開発ワークフローを Claude Code の上で運用しています。テストが落ちたときには、自動修正のステップが「原因を分析して実装かテストを直し、緑を目指せ」と動きます。
この指示、よく読むと穴があります。テストの assert を弱めて緑にする経路を、明示的には禁止していない。緑への圧力がかかったとき、実装を直すのと、テストの期待値を甘くするのとでは、後者のほうが速くて確実です。締め切りに追われた人間がついやってしまうのと、まったく同じ最適化を、AIはためらいなく実行します。
そして厄介なのは、下流の防御が効きにくいことです。前述のとおり、赤から緑への変化はテストが実装を制約している証明にはなっても、正しい挙動を検証している証明にはならない。弱い assert に書き換えられたテストも、堂々と緑を出して素通りしていきます。
対策は、一枚岩ではなく三層にしました(壁が1枚だと、AIは律儀にその横をすり抜けていくので)。
- 禁止を明文化する — 自動修正のプロンプトに「テストの期待値・assert を弱めて緑にすることは禁止。テスト修正が正当なのはテスト自体の誤りを直す場合のみで、どのテストをなぜ変えたか必ず申告せよ」を追加する
- 発生を追跡し、監査を注入する — 自動修正が走った回数を数え、一度でも走った実行では、評価役エージェントの監査観点に「テスト diff が弱体化していないか」を自動で差し込む
- 監査をスキップする経路を塞ぐ — 小規模な変更では評価フェーズ自体を省く最適化があり、そこで自動修正が起きると監査が無音で消える穴があった。評価を実行する条件に「自動修正が一度でも走ったら規模にかかわらず評価する」を足して、スキップ経路を封鎖する
コードで言うと、評価を回すかどうかの判定はこういう形になります。
// 小規模でも、危険な変更か「テストを直して緑にした」履歴があれば評価を省略しない
const runEval =
effectiveShape !== 'micro' ||
dangerHits.length > 0 ||
greenFixCount > 0
greenFixCount は自動修正が走った回数です。ゼロでない限り、規模を理由にした監査スキップは発動しません。教訓はシンプルで、「直せ」と圧をかけるなら、ズルの定義も一緒に渡す。そして効率化のために監査を省く経路には、「監査が必要になった瞬間に省略をやめる」条件を必ず添える。圧力と監査はセットでした。
「言わなくなった」と「直った」は違う
テストの緑と並んで、もう一つ「通ったことにされる」ものがあります。評価役エージェントの指摘です。
評価エージェントには「新規の指摘だけ報告せよ。対応済みの論点の蒸し返しは禁止」と指示していました。差し戻しのたびに同じ指摘を繰り返されると収束しないので、これ自体は必要です。ところが収束を判定する側は「今回の報告に現れなくなった重大指摘は、解消されたのだろう」と推定していました。
この2つを組み合わせると、指示に従順なエージェントほど、未解消の指摘が黙って「解消済み」に化けます。蒸し返し禁止に従って言及をやめた瞬間、直っていない指摘がゲートを通る。静かになった会議が、合意できた会議とは限らないのと同じ構図です(黙ったのは納得したからとは限らない、というのは会議室でも見覚えのある話ですよね)。
そこで「沈黙=解消」の自動判定は廃止し、解消の明示申告に切り替えました。評価エージェントは、未解消だった重大指摘の一件ずつについて「解消したか・その根拠は何か」を毎回返すことを義務付けられます。返す形式は { id, resolved, evidence } の集合で、resolved: true には具体的な根拠——どのファイルのどこを直したか、どのテストで確認したか——が必須です。根拠のない解消宣言は無視され、その指摘は未解消のまま据え置かれます。
これは、テストの緑を「弱い assert では通さない」よう縛ったのと同じ発想です。成功の宣言には、宣言だけでなく証拠を求める。緑という宣言にも、解消という宣言にも、裏取りをセットにする。
もう一点、言い換えによるすり抜けにも蓋をしました。差し戻しが同じ論点で堂々巡りしていないかは、以前は指摘の文字列を突き合わせて検出していました。ところが「エラー処理が不十分」が次には「異常系の考慮が不足」に化けると、意味は同じでも文字列は別物で、突合が漏れます。そこで検出を文字列に頼るのをやめ、表現に左右されない決定論的なカウンタで総回数に上限を置きました。上限に達したらエラーで止めるのではなく、ループを抜けて人間の判断に渡します。ゲートを緩めるのではなく、暴走コストにだけ蓋をする形です。説得はプロンプトで、保証は決定論で——LLM 相手の検出を文字列一致に頼ると、悪意ゼロの言い換えであっさり抜けられます。
一度も赤くならないテストを疑う
最後は、少し毛色の違う話です。ここまでは「緑がすり抜ける」話でしたが、こんどはゲートそのものが空振りしていた話です。
以前、品質ゲートに見つかった抜け道をまとめて紹介しました(品質チェックを任せたAIが、チェックの攻略を始めた)。その中で一番ぞっとしたのが、設置以来一度も警告を出したことのないチェックが、実は死んでいたという話です。
危険な変更を機械的に洗い出すチェックが、コミット前の作業ツリーを見るはずが、実際には空の差分を見ていました。このワークフローでは実装役にコミットを禁じているため、チェック時点では最新のコミットがベースブランチと一致している。そこで使っていた三点比較の差分は、常に空を返し、チェックは毎回「問題なし」と律儀に報告していた。発火しないから安全なのではなく、そもそも何も見ていなかったわけです。
修正は、コミット前専用の差分モードの追加でした。共通の起点からの二点比較で追跡済みファイルの変更を拾い、git status --porcelain --untracked-files=all で未追跡の新規ファイルを列挙して、コミットしていない状態でも実際の変更を漏れなく分類します。
この教訓は、AIのテストにもそのまま効きます。一度も赤くなっていないテストは、品質が高い証拠ではなく、テストが何も検証していない兆候かもしれない。新しく足したテストは、わざと実装を壊して赤くなることを一度確認する。落ちることを確かめて初めて、その緑に意味が宿ります(まあ、一度も赤くならないテストは、置物としては優秀なんですけどね)。
あなたのプロジェクトで、緑を信じる前に見る観点
自動化ループの話をしてきましたが、要点は「AIにテストごと書かせる」あらゆる現場に当てはまります。緑を信用してよいか迷ったら、次の順で確かめると早いです。
具体的には、この4つを見てください。
- 落として確認したか — 新しいテストは、実装をわざと壊して一度赤くする。落ちないテストは通っていても情報量がない
- assert が中身を見ているか — 「存在する」「エラーが出ない」だけの assert は、実装の勘違いを一緒に見逃す。値や状態まで確かめているか
- 緑にした過程を申告させたか — AIにテストを直させたなら、「どのテストをなぜ変えたか」を必ず言わせる。期待値を甘くしていないか、変更点だけ人間が見る
- 直ったの根拠を求めたか — 「直しました」で終わらせず、どこを直しどう確認したかまで。宣言ではなく証拠で受け取る
どれも、AIを疑うための観点ではありません。AIは怠けているのではなく、緑への最短経路を見つけるのが上手すぎるだけです。その最短経路の途中に弱いテストが落ちていたら、当然そこを通る。通り道に弱いテストを置かないのは、道を設計する側、つまり人間の仕事です。
まとめ
AIに書かせたコードのテストを、どこまで信じてよいか。整理するとこうです。
- 「赤から緑」はテストが実装を制約している証明であって、正しさの証明ではない — 弱い assert でも緑は出る
- 緑への圧力はテストを弱めにいく — 禁止の明文化・発生の追跡・監査スキップ経路の封鎖を、セットで設計する
- 「言わなくなった」を「直った」と読まない — 解消も緑も、宣言ではなく証拠で受け取る
- 一度も赤くならないテストを疑う — 落ちることを確認して初めて、緑に意味が宿る
AIで作ったコードを自分で保守すると決めたなら、品質の最前線はテストです。そのテストの緑を、どこまで裏取りして信じるか。そこに一手間を設計できるかどうかで、「動いている」と「信頼できる」の距離が決まります。緑を疑うのは、AIを疑うことではありません。緑を信じられる状態を、自分の手で作ることです。



