CIパイプラインで「再実行したら通った」テストに悩んでいるエンジニアに向けた内容です。マイクロサービス構成でE2Eテストやインテグレーションテストを回している方なら、身に覚えがあるはずです。
Flaky test(フレーキーテスト、同じコード・同じ条件でも成功と失敗が入れ替わる不安定なテスト)は、CI全体の信頼性を静かに削っていきます。開発者がPRのたびに再実行ボタンを押し続け、テスト結果がリリース判断の根拠として使えなくなる状態です。フロントエンドでもWebDriverベースのUIテストやAPIモックを絡めたインテグレーションテストで頻発する現象で、決して珍しい話ではありません。
何が起きているか:症状の広がり方
Flaky testの症状は現場によらずよく似ています。特定のテストだけがCIで時々落ち、ローカルでは何度実行しても再現しない。並列実行を有効にすると失敗率が上がる。特定のCIランナーでだけ落ちる、といったパターンです。
影響範囲は個々のテストに留まりません。開発者がCIの赤を「どうせflakyだから」と無視する習慣がつくと、本当のリグレッションを見逃すリスクが上がります。テストスイート全体の信頼性が崩れる、という意味で本質的には障害対応と同じ扱いが必要な問題です。
なぜ起きるか:原因を段階的に分解する
原因は大きく5つのパターンに分解できます。それぞれ症状の出方が違うので、切り分けの第一歩として整理しておきます。
- 競合状態(race condition): テストが処理順序やタイミングに暗黙的に依存している場合、CIのスケジューリング差でだけ失敗する
- 非決定的な環境・データ: 共有DB、グローバルな時刻取得、ランダムシード、可変なfixtureが実行順によって結果を変える
- 外部依存の不安定さ: 呼び出し先APIのレート制限やネットワークの揺らぎがそのままテスト結果に混入する
- テストの肥大化: インテグレーションテストやUIテストは可動部分が多く、リソース負荷にも弱い
- テストフレームワーク自体の脆さ: WebDriverの操作待ちやエミュレータの不安定さが、コードとは無関係に失敗を生む
この中でフロントエンド開発者が特に踏みやすいのが、テストの肥大化とWebDriver周りの脆さです。Cypress や Playwright を使ったE2Eテストで、要素のレンダリング完了を待たずにクリックしてしまうケースはこの典型です。見た目は「たまに失敗する謎の不具合」ですが、原因は単純な待機不足だったりします。
自分のプロジェクトが該当するか確認する
本格的に対策する前に、現状を数値で把握しておくと判断が早くなります。
まず、疑わしいテストを同一のCI用コンテナイメージ上で繰り返し実行し、失敗率を測定します。
for i in $(seq 1 50); do
./run-tests single TestClass#testMethod || true
doneこのループを回して失敗回数をカウントすれば、100回に1回未満のごく稀な失敗なのか、10回に1回程度の頻発型なのかが分かります。頻発型なら原因の切り分けが容易ですが、100回に1回未満の場合は自動トリアージ自体が難しくなるため、優先度を下げて他の頻発テストから着手するのが現実的です。
次に確認したいのが、失敗が特定のCIノードに集中しているかどうかです。複数の同一構成ノードで同じテストを流し、ノード固有の問題か、テスト自体の設計問題かを見極めます。
さらに、依存先サービスを一時的に切り離して確認する方法も有効です。WireMock のようなAPIモックツールや、Testcontainers のような使い捨てDBコンテナに置き換えて実行し、結果が安定するなら外部依存が原因、安定しないならテストコード側の問題だと判断できます。
フロントエンドのE2Eテストであれば、playwright test --repeat-each=20 のようなオプションで同一テストを繰り返し実行し、失敗パターンを収集する方法も使えます(オプション名はツールのバージョンにより異なるため、使用しているPlaywright/Cypressのバージョンとオプション一覧は公式ドキュメントで確認してください)。
対策の手順
原因が特定できたら、パターンごとに定石となる修正を当てていきます。
1. 競合状態・タイミング依存の解消
sleep() による時間待ちは応急処置にしかなりません。代わりに、要素の状態変化やAPIレスポンスの完了を待つ明示的なポーリング(Playwrightの waitForSelector やCypressの cy.intercept に相当する仕組み)に置き換えます。時間ベースの待機は環境負荷が変わると簡単に破綻します。
2. 非決定的なデータの排除
テスト用データにランダム値を使う場合は、シード値を固定して再現性を確保します。共有DBを使っている場合は、テストごとに独立したスキーマやコンテナを用意し、他のテストの状態に影響されない構成に変えます。
3. 外部依存のモック化
サードパーティAPIやマイクロサービス間通信をテストで直接叩いている場合、WireMockやMSW(Mock Service Worker、フロントエンドのネットワークリクエストをブラウザ層でモックする仕組み)のようなツールで固定レスポンスに切り替えます。これにより本番APIのレート制限やネットワーク揺らぎの影響を受けなくなります。
4. テストの分割
1つのテストに多くのアサーションや操作を詰め込んでいる場合は分割します。可動部分が減るほど失敗要因の特定が容易になり、実行時間も短縮できます。
5. CI側の運用ルール整備
修正が難しいテストは、CIをブロックする「gating」対象から一時的に外し、「quarantine(検疫)」タグを付けて別枠で監視する運用が現実的です。ただし検疫のまま放置すると死んだテストが増えるだけなので、検疫リストの棚卸し日を決めておくことが欠かせません。再実行(retry)を使う場合も、無条件の再試行ではなく失敗回数と原因タグを記録し、後から傾向を追えるようにしておきます。
まとめ
Flaky testはテストコードの品質問題であると同時に、CI基盤やテスト設計の健全性を映す指標でもあります。
まず疑わしいテストを同一環境で50回程度繰り返し実行し、失敗率を測るところから始めてみてください。
失敗パターンが競合状態、データの非決定性、外部依存、テストの肥大化のどれに当たるかを切り分けたら、対応する修正パターンを一つずつ当てていくことで、CIの赤を安心して信頼できる状態に戻していけます。