LEDが点灯するネットワークスイッチのポート接写
ニュース深掘り

マイクロサービスのFlakyテスト、CIで再現できない原因を切り分ける手順

目次を見る

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テストで、要素のレンダリング完了を待たずにクリックしてしまうケースはこの典型です。見た目は「たまに失敗する謎の不具合」ですが、原因は単純な待機不足だったりします。

flakyテストは「たまたま落ちた」ではなく、テストかインフラのどちらかが発している正常な信号として扱う必要があります。

自分のプロジェクトが該当するか確認する

本格的に対策する前に、現状を数値で把握しておくと判断が早くなります。

まず、疑わしいテストを同一の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の赤を安心して信頼できる状態に戻していけます。

参考

Diagnosing and Fixing Flaky Microservice Tests

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。