CIで赤くなったテストを再実行したら緑に戻ってしまい、原因がつかめず落ち着いてログを見返す開発者

たまに落ちるテスト(Flaky)の原因をAIに切り分けさせる手順

CIを回すたびに、同じテストが赤くなったり緑になったりする。 コードは変えていない。再実行したら通る。でも次の日にはまた落ちている。原因もわからないまま「とりあえずもう一回」を押している——そんな状態、ありませんか。

この「たまに落ちる」テストは、Flaky(フレーキー、気まぐれな)テストと呼ばれます。落ちる理由が毎回違って見えるので、追いかけるのがとても厄介です。放っておくと「どうせまた気まぐれだろう」とチームが赤信号を無視するようになり、本物のバグまで見逃してしまう。かといって、腰を据えて原因を追うには時間も気力も要ります。そこで頼りたいのがAIですが、Flakyは丸投げでは解けません。AIは手元でCIを回せず、あなたの環境を見られないからです。この記事では、AIに「原因の切り分け役」を担ってもらいながら、一緒に犯人を絞り込んでいく手順を、コピーして使える形で整理します。

結論:Flakyテストは「気まぐれ」ではなく、必ず原因があります。多くは決まった型に収まる——①時間・順序への依存sleep頼み、テスト同士の実行順、日付や現在時刻)、②共有状態の汚染(DB・グローバル変数・ファイルを前のテストが残す)、③外部依存(ネットワーク・外部API・時計)、④並列実行の競合、⑤乱数・順序が不定な集合(Set/Mapの順、未ソートの結果)。切り分けは3ステップ。まず落ちたときのログとテスト内容をAIに渡し、この5類型のどれに当たりそうか仮説を出させる。次にその仮説を確かめる小さな実験(単体で回す・順番を変える・10回連続で回す・時刻を固定する)をAIに設計してもらい、自分の環境で実行する。最後に再現できた原因だけを直す。AIに丸投げして「直しといて」ではなく、仮説→実験→確認をこちらが回し、AIには型の当てはめと実験の設計を手伝ってもらう。これがいちばん速くて確実です。

「たまに落ちる」と聞くと運のように感じますが、コンピュータに気まぐれはありません。 同じ入力なら同じ結果になるのが本来の姿で、結果がブレるのはどこかに「毎回同じでない何か」が紛れ込んでいる証拠です。時間、順序、共有された状態、外の世界。犯人はだいたいこのあたりに隠れています。今日は、その隠れ場所を一つずつ照らしていきましょう。

Flakyテストの原因は、だいたい5つの型に収まる

たまに落ちるテストの主な原因を、時間・共有状態・外部依存・並列・順序不定の5つに分けて示した図

Flakyの原因は無限にあるように見えて、実際は決まった所に集まります。まず、この5つの型を頭の隅に置いておくと、切り分けがぐっと楽になります。

① 時間・順序への依存 sleep(100) で「たぶん処理が終わっているはず」と待つ書き方は、遅い日に間に合わず落ちます。テストの実行順に依存している場合(Aが先に走ると通るが、Bが先だと落ちる)や、現在時刻・日付に依存している場合(月末や日付境界で結果が変わる)も、ここに入ります。

② 共有状態の汚染 前のテストが残したデータや設定を、次のテストが拾ってしまうパターン。DBのレコード、グローバル変数、環境変数、一時ファイル、キャッシュ。「単体で回すと通るのに、まとめて回すと落ちる」なら、まずこれを疑います。

③ 外部依存 ネットワーク、外部API、時計、乱数の種など、テストの外にあってこちらで固定できないものに触れている。相手が遅い・落ちている・応答が変わると、テストも道連れになります。

④ 並列実行の競合 テストを並列で走らせているとき、同じDBやファイル、同じポートを複数のテストが取り合うと、タイミング次第で落ちます。直列なら通るのに並列で落ちるなら、ここが濃厚です。

⑤ 乱数・順序が不定な集合 SetやMap、ハッシュの走査順、SQLでORDER BYを付けない結果は、実行ごとに順番が変わることがある。それを「この順で来るはず」と決め打ちして比較していると、たまにズレて落ちます。

覚えるというより、落ちたテストを前にして上から当てていくチェックリストとして使ってください。次の章から、この当てはめをAIに手伝ってもらいます。

手順①:落ちたログとテストをAIに渡し、仮説を出させる

最初にやるのは、犯人を決めることではなく、容疑者を絞ることです。ここはAIが得意な所。落ちたときの情報をまとめて渡し、5類型のどれに当たりそうか仮説を出させます。

AIに渡すと切り分けが進むものは、次の4点です。

聞き方の例:

このテストが「たまに落ちる(Flaky)」状態です。原因をまだ断定せず、
考えられる原因を可能性の高い順に3つ、仮説として挙げてください。

次の観点で分類してください:
① 時間・順序依存(sleep・実行順・現在時刻)
② 共有状態の汚染(DB・グローバル変数・ファイル・キャッシュ)
③ 外部依存(ネットワーク・外部API・時計・乱数)
④ 並列実行の競合
⑤ 順序が不定な集合(Set/Map・ORDER BYなし)

各仮説について「なぜそう考えたか」と「それを確かめる最小の実験」も添えてください。
まだコードは直さないでください。

--- テストコード ---
(貼る)
--- 失敗時のスタックトレース ---
(貼る)
--- 落ち方の傾向 ---
再実行すると通る。単体だと通るが、テスト全体で回すとたまに落ちる。

大事なのは、「直して」ではなく「仮説を挙げて」と頼むこと。 いきなり修正を頼むと、AIは原因を確かめないまま、それらしい変更(sleepを伸ばす等)を返しがちです。それはたいてい、落ちる確率を下げるだけで、根っこは残ります。まずは容疑者リストを作る。そのうえで、次の手順で一つずつアリバイを崩していきます。スタックトレースの読み解きが難しいときはバグ調査でAIにスタックトレースを読ませるときのコツも合わせてどうぞ。

手順②:仮説を確かめる「小さな実験」を回す

仮説が出たら、次は確かめる番です。ここは、AIには代われない部分——AIはあなたのCIやDBを触れないので、実験は自分の手で回します。とはいえ、実験の設計はAIに手伝ってもらえます。

型ごとに、効く実験はだいたい決まっています。

そして、Flaky退治でいちばん効くのが、「連続で回して再現率を測る」という実験です。

このテストを同じ条件で連続実行して、落ちる頻度を確認したいです。
(使っているテストフレームワークを伝える)で、同じテストを20回連続、
また50回連続で回すコマンドと、失敗回数を集計する方法を教えてください。
並列度を1に固定する方法、実行順をランダム化する方法も教えてください。

「20回中3回落ちる」のように再現率が数字で見えると、切り分けが一気に進みます。単体で20回回して0回、全体で20回回して3回なら、犯人は「他のテストとの相互作用」だと絞れる。並列度1で0回、並列で落ちるなら、競合で確定。AIには実験の手順を作ってもらい、実際に回して数字を取るのは自分。この役割分担が、Flaky切り分けの要です。

手順③:再現できた原因だけを直す(確率を下げる対症療法で終わらせない)

実験で原因が再現できたら、いよいよ修正です。ここで守りたい原則は一つ——再現・確認できた原因だけを直す

Flakyでいちばんやりがちなのが、「とりあえずsleepを長くする」「落ちたらリトライする」で確率を下げて終わらせること。これは根っこを残したまま、症状を薄めているだけです。忙しい日には効くように見えても、遅いマシンや混んだCIで、また顔を出します。型ごとの「根っこを断つ直し方」は、だいたい次の向きです。

直したら、必ず手順②の連続実行をもう一度。20回・50回回して落ちが消えたかを、数字で確認します。「たぶん直った」で閉じないのがコツ。直す前に再現率を測り、直した後にも測って、ゼロになったことを確かめる。ここまでやって、はじめてそのFlakyは片付いたと言えます。

AIに修正案を出させる場合も、最後の確認は人の仕事です。AIは実験結果を見ていないので、再現していない原因まで「念のため」直そうとすることがあります。あなたが実験で確かめた原因に絞って直してもらう——AIとの修正ループが終わらないとき堂々巡りから抜ける手順にも通じる、範囲を切る構えが効きます。

ありがちな落とし穴と、その回避

FlakyをAIと切り分けるとき、つまずきやすい所を先に潰しておきます。

落とし穴の多くは、「早く赤を消したい」という気持ちから、確かめる手順を飛ばす所で生まれます。 急がば回れで、再現率を測る一手を挟むだけで、同じテストに何度も戻される消耗がぐっと減ります。

明日からやること(小さく始める3つ)

いきなり全部のFlakyを片付けなくて大丈夫です。まずこの3つから。

  1. 次に「たまに落ちる」テストに出会ったら、sleepを伸ばす前にAIへ「仮説を3つ」頼む:5類型のどれかに当てはめてもらうだけで、追う方向が定まります。
  2. 落ちるテストを、単体で20回・全体で20回、連続実行してみる:再現率の差で「他のテストとの相互作用か」が見えます。実験コマンドはAIに聞けば作ってくれます。
  3. 直したら、もう一度連続実行して再現率がゼロになったか確かめる:「たぶん直った」で閉じない。数字で見届けます。

この3つだけでも、「気まぐれで手に負えない」と感じていたテストが、少しずつ「原因のわかる不具合」に変わっていきます。

コピーして使う「Flaky切り分けシート」

落ちたテストを前にしたら、上から順に埋めてください。全部でなく、わかる所からで十分です。

# Flakyテスト 切り分けシート

## 1. 落ち方を観察する
- [ ] 毎回同じ所で落ちる? / 落ちる場所が変わる?
- [ ] 再実行すると通る?
- [ ] 単体だと通る? 全体で回すと落ちる?
- [ ] 手元でも落ちる? CIだけ?
- [ ] 並列で回している? / 直列だと?

## 2. 仮説を立てる(5類型のどれか)
- [ ] ① 時間・順序依存(sleep・実行順・現在時刻)
- [ ] ② 共有状態の汚染(DB・変数・ファイル・キャッシュ)
- [ ] ③ 外部依存(ネットワーク・API・時計・乱数)
- [ ] ④ 並列実行の競合
- [ ] ⑤ 順序が不定な集合(Set/Map・ORDER BYなし)

## 3. 実験で確かめる(再現率を数字に)
- [ ] 単体で20回連続 → 失敗 __回
- [ ] 全体で20回連続 → 失敗 __回
- [ ] 並列度1で20回 → 失敗 __回
- [ ] 外部をモックにして20回 → 失敗 __回
- [ ] 時刻を固定して/ソートして20回 → 失敗 __回

## 4. 直して、また測る
- [ ] 再現できた原因だけを直したか
- [ ] 対症療法(sleep延長・リトライ)で終わらせていないか
- [ ] 直した後、連続実行で失敗が0回になったか

最後に

再実行のボタンを押しながら、「またこのテストか」とため息をつく。 その気持ち、よくわかります。でも、あなたが「気まぐれで済ませたくない」と思っている時点で、チームの品質はちゃんと守られています。赤信号を無視せずに向き合おうとしているのは、簡単なようで、いちばん大事な姿勢です。

Flakyテストの原因を突き止めてCIが安定して緑になり、肩の力が抜けて穏やかに次の作業へ向かう開発者

一度に全部のFlakyを消さなくて大丈夫です。今日ひとつ、「sleepを伸ばす前に、連続実行で再現率を測る」だけ試してみる。それだけで、気まぐれに見えた不安定さが、原因のたどれる不具合に変わります。AIには型の当てはめと実験の設計を、あなたは再現と確認を。この分担さえ持てば、たまに落ちるテストは、もう手に負えない相手ではありません。

よければ、こちらも

関連用語