テストは通るのに実際は壊れている:AI製コードで起きる原因の切り分け表
AIを使いこなすClaudeで執筆し、GPTで自動校閲・修正しています。

結論から言うと、テストが緑なのは「実行されたテストが、書かれた確認の範囲で失敗しなかった」という意味でしかありません。実際に動かないなら、原因は次の三か所のどれかにあります。 • テスト側:壊れている部分を見ていない、そもそも実行されていない、AIが期待値を実装に合わせて書いている • 環境側:手元と本番・CIで、OS、Nodeのバージョン、依存パッケージ、環境変数、時刻などが違う • コード側:テストでは隠れる非同期処理の取りこぼしや、モックの外にある実処理の誤り AIにコードもテストもまとめて書かせると、同じ思い込みが両方に入りやすくなります。そのため「テストが通った」を根拠にせず、先にテストの信頼性を確認してから原因を探すのが近道です。この記事では、症状から疑う場所を引ける表と、確認の順番、AIに渡す依頼文を示します。例はNode.js(JavaScript)を想定しています。
この記事の目次
まず「緑」が何を保証しているかを確認する
Node.js標準のテストランナーを例にすると、公式ドキュメントには、緑のまま通り抜ける仕組みがいくつか書かれています。
- skipを付けたテストは実行されません。todoを付けたテストは失敗してもテスト全体を失敗扱いにしません。(Node.js: Test runner)
- --test-only付きで起動すると、onlyが付いたテストだけが走ります。AIが絞り込み用にonlyを残していると、他は動いていないのに緑になり得ます。
- テスト内で完了を待たない非同期処理は、成否の確認から漏れることがあります。
- 既定では、*.test.jsやtest/配下など決まったパターンに合うファイルだけが対象です。名前や置き場所がずれたファイルは、書かれていても拾われない可能性があります。
つまり「0件失敗」と「全部確認した」は別物です。緑の画面を見たら、まず実行件数とスキップ件数を見てください。
症状別の切り分け表
下の表は、この記事の編集上の整理です(実測データではありません)。上から当てはまる症状を探し、「最初の確認」だけ先に試してください。
- 実行件数が想定より少ない、または0件、疑う場所:テスト、最初の確認:ファイル名・置き場所が検出パターンに合うか。skip/todo/onlyが残っていないか
- 手元では動くがCIや本番で動かない、疑う場所:環境、最初の確認:Node・OS・依存のバージョン差。CIではCI変数がtrueになる点も含め、環境変数の差を見る
- CIも通るが本番だけ動かない、疑う場所:環境、最初の確認:本番の設定値・権限・接続先。テストがモックで済ませている部分
- 画面やAPIの結果が空、または未完了の状態、疑う場所:コード、最初の確認:awaitやreturnの漏れ(下の例を参照)
- たまに失敗する、実行順で結果が変わる、疑う場所:テスト/コード、最初の確認:時刻・乱数・共有状態・待ち時間に頼っていないか
- 特定の日付・月末・年初だけおかしい、疑う場所:環境/コード、最初の確認:現在時刻を固定していないか。タイムゾーンに依存していないか
- 正常な入力は通るが、空・長い・特殊な値で壊れる、疑う場所:テスト、最初の確認:異常系のテストがあるか。期待値が入力例の1パターンだけでないか
- テストを直すたびにAIがテストの期待値を変える、疑う場所:テスト、最初の確認:期待値が仕様ではなく実装の出力から作られていないか
- 外部API・DB・ファイルを使う処理が動かない、疑う場所:コード/環境、最初の確認:モックが本物の応答と同じ形か。実環境の最小限の確認を1回通す
- 依存を足した直後から不安定、疑う場所:環境、最初の確認:lockfileの差分。別の環境でlockfile通りに入れ直して再現するか
最初に試す確認の順番
原因を当てにいく前に、安くて結果がはっきりする順に確認します。
- 再現手順を1行で固定する:「どの操作で、何が表示され、本来は何になるべきか」を書きます。ここが曖昧だと、AIも人も別の問題を直し始めます。
- テストの実行件数を見る:実行・成功・スキップ・todoの内訳を確認します。onlyやskipの検索(grep)も有効です。
- 壊れる入力をテストに1件足す:再現手順をそのままテストにします。これが赤になれば、テストの穴が原因だと分かります。緑のままなら、テストが実際の経路を通っていない可能性があります。
- 環境を揃えて再現する:Nodeのバージョン、lockfile、環境変数を問題が出る側に合わせます。ここで再現すれば環境側です。
- モックを外して1回だけ動かす:APIやDBの代わりを本物(または検証用の環境)に替えます。通れば、モックの形が実物とずれていたことになります。
- 非同期の取りこぼしを探す:awaitのない呼び出し、returnのないthen、forEach内のasyncなどを確認します。
説明用の例:`return`のない処理は、テストが緑でも結果が空になる
次は、切り分けの感覚を示すための架空の例です(実際に実行して確認したものではありません)。
// 説明用の例
export function loadNames(fetchUsers) {
const names = [];
fetchUsers().then((users) => {
users.forEach((u) => names.push(u.name));
});
return names; // 取得完了を待たずに空の配列を返す
}このコードを呼ぶテストが「配列が返ること」しか確認していなければ緑になります。しかしこの例では、fetchUsers()の完了を待たずに空の配列を返します。MDNは、.then()の中で開始したPromiseをreturnしないと、その完了を追跡できない「floating promise」になると説明しています(MDN: Using promises)。
この例では、関数をasyncにしてawait fetchUsers()の結果を返す形が自然です。テストも「中身が期待どおり入っている」ことを確認するよう変えます。ここで重要なのは、修正より先に、中身が空だと赤になるテストを書くことです。
AIに渡す依頼文の例
原因が分からないうちに「直して」と頼むと、AIはテストを緑にする方向へ寄りがちです。次のように、調べる範囲と禁止事項を先に渡します。
テストは全て通るのに、次の操作で不具合が出ます。
【再現手順】(1行で)
【実際の結果】
【期待する結果】
【環境】Nodeのバージョン、OS、実行場所(手元/CI/本番)
次の順で進めてください。
1. まずコードは変更せず、テストの実行件数、skip/todo/onlyの有無、
テスト対象のファイルが検出されているかを報告する。
2. 再現手順をそのまま再現する失敗テストを1件追加する。
既存テストの期待値は変更しない。
3. 失敗の原因を、テスト・環境・コードのどれかに分類し、
根拠(ログや該当行)を示す。
4. 修正案を出し、私が承認してから変更する。秘密情報や実データは貼らず、ダミーの値に置き換えてください。AIが「原因はこれです」と言い切っても、再現テストが赤から緑に変わる様子を、ご自身の環境で確認するまでは仮説として扱ってください。
直ったと判断する前に人が見る点
- 追加した再現テストが、修正前のコードで確かに赤だったか
- 既存テストの期待値やskipの追加が、差分(git diff)に混ざっていないか
- 問題が出ていた環境(CI、本番相当)で、同じ手順をやり直して直ったか
- モックを使うテストだけでなく、実際の経路を最低1回通したか
特にAIが修正のついでにテストを書き換えていないかは、差分で必ず見てください。期待値が動いていたら、その理由が仕様の変更かどうかを確認します。理由が説明できない変更は、元に戻す対象です。
なお、CI側の環境変数は、GitHub ActionsではCIやGITHUB_ACTIONSが自動でtrueに設定されます(GitHub Docs: Variables reference)。コードがこれらを見て挙動を変えている場合は、手元の実行と別の経路を通っていることになります。手元でCI=trueを付けて実行すると、差を試せます。

