Flakyテスト地獄から脱出する方法|原因5分類・診断から再発防止まで解説

「テストが落ちても本当のバグかどうかわからない」「CIが赤くても誰も気にしなくなった」——そのFlakyテスト地獄から脱出する方法を、原因分類・診断・対策・再発防止まで実務経験をもとに解説します。Wait設計・環境差異・データ競合・外部依存など、Flakyの種類別に優先順位付きで対策を紹介します。

Flakyテストの本当の怖さは「テストが落ちること」ではなく、「テスト結果を誰も信頼しなくなること」です。

📌 この記事はこんな方におすすめ

  • 「CIが落ちてもとりあえずリランする」が習慣になっているQAエンジニア
  • Flakyが多すぎてテスト結果への信頼が失われているチームのリード
  • Flakyが「なぜ起きているか」の原因を体系的に理解したい方
  • Flakyの再発防止まで含めた根本解決を目指している方

✅ この記事を読むと得られること

  • Flakyテストの種類分類と、それぞれの根本原因がわかる
  • 「どのFlakyから優先して直すか」の判断基準がわかる
  • 原因別の具体的な対策コードと再発防止の設計パターンがわかる

👤 この記事を書いた人

QAエンジニア・テスト自動化エンジニアとして15年以上の実務経験を持つ Yoshi が執筆。「CIが赤くても誰も気にしない」状態まで悪化したFlakyテスト地獄を、複数のプロジェクトで経験・復旧してきた実体験をもとに解説します。

📖 関連記事との使い分け

  • テスト自動化でよくある失敗5選:Flakyを含む5つの失敗を概説 → 自動化全体の失敗パターンを知りたい方はこちら
  • Selenium運用崩壊した話7選:SeleniumのWait混在・ChromeDriver起因のFlaky → Selenium特有のFlakyはこちら
  • この記事:ツール非依存でFlakyの原因を分類し、診断から根本解決・再発防止まで体系的に解説

📌 結論(3つのポイント)

  • FlakyテストはWait・環境・データ・外部依存・設計の5種類に分類でき、種類が違えば対策も違う
  • 「とりあえずリラン」は応急処置。根本解決は「どの種類のFlakyか」を特定することから始まる
  • Flakyが消えないなら「自動化すべきでないテスト」の可能性がある。削除も正解のひとつ

「またFlakyか」——その一言が日常になったとき、テスト自動化は機能を失っています。CIが赤くても「どうせFlakyだろう」とリランするだけ。本当のバグを見逃すリスクが高まり、やがて誰もテスト結果を信頼しなくなります。

この記事ではFlakyテストを「なんとなく直す」のではなく、原因を種類ごとに分類して体系的に脱出する方法を解説します。

Flakyテストとは何か?なぜ危険なのか

Flakyテスト(Flaky Test)とは、「同じコードで同じ環境のはずなのに、実行するたびに合否が変わる不安定なテスト」のことです。

Flakyが危険なのは、テストが落ちること自体よりも「テスト結果への信頼が失われること」にあります。

段階チームの状態リスク
初期「なんか落ちたけどリランしたら通った」軽微
中期「どうせFlakyだろう」と確認せずにマージ⚠️ バグ見逃しリスク
末期「CIが赤くても誰も気にしない」🔴 自動化の価値がゼロに

Flakyテストの種類と根本原因とは?5分類

Flakyテストを「なんとなく不安定」で片付けると永遠に直りません。まずどの種類のFlakyかを特定することが根本解決の第一歩です。

種類典型的な症状検知方法主な発生ツール修正難易度
① Wait系「要素が見つからない」「クリックできない」リランで通るSelenium / Playwright🟢 比較的低
② 環境依存系「ローカルは通るがCIで落ちる」CI環境でのみ失敗Selenium / Playwright🟡 中
③ データ競合系「並列実行時だけ落ちる」並列実行時のみ失敗全般🔴 高
④ 外部依存系「外部APIのレスポンスが遅いと落ちる」特定時間帯・外部障害時に集中API / E2Eテスト🟡 中
⑤ 設計起因系「テスト順序を変えると落ちる」実行順変更で再現(pytest –randomly-seed=random)全般🔴 高

まずここを確認:症状からFlakyの種類を診断するとは?

「うちのFlakyはどの種類?」——この表で症状から診断してください。原因の種類が分かれば、対策のSTEPに直接ジャンプできます。

こんな症状があるまず疑うFlaky種類対策STEP
リランしたら通った① Wait系→ STEP 2へ
ローカルは通るがCIだけ落ちる② 環境依存系→ STEP 3へ
並列実行(pytest-xdist)時のみ落ちる③ データ競合系→ STEP 4へ
特定の時間帯・外部サービス障害時に集中する④ 外部依存系→ STEP 5へ
テスト順序を変えると再現する⑤ 設計起因系→ STEP 6へ
どの条件でも再現できない・対策しても再発隔離・削除を検討→ STEP 7へ

STEP 1:まずFlakyを計測・可視化するとは?

「直す前にまず数える」——これが地獄脱出の最初のステップです。感覚ではなくデータとして把握することで、どこから手をつけるかの優先順位が決まります。

計測すべき3つの指標

指標計測方法判断の目安
Flaky率「失敗した回数 ÷ 実行した回数」5%以上は要対処の目安(チーム規模・CI頻度・リラン運用によって変わりますが、多くの現場ではこのあたりから「CIを信用しなくなる」傾向が強まります)
影響テスト数CIの失敗ログから集計全テストの10%超は危険域
CI ブロック時間リランが必要だった回数×平均実行時間週1時間超は業務コスト化
# pytest-repeat を使って同じテストを複数回実行し、Flaky率を計測する
# ⚠️ pytest標準機能ではありません。事前にプラグインのインストールが必要です
# pip install pytest-repeat

# 5回繰り返し実行(Flakyなら数回に1回失敗する)
# ※ CI組み込み時は実行時間が増えるため、Flaky調査用途での限定使用を推奨
pytest tests/test_login.py --count=5

# 結果例:5回中2回失敗 → Flaky率40%(要対処)
# PASSED(3回)FAILED(2回)

# GitHub Actions のCI結果を集計してFlaky率を可視化する場合
# pytest --junitxml=results.xml で結果を保存
# Datadog / Grafana / Allure Report / ReportPortal などに取り込むと長期トレンドが見やすい
💡 実務Tip:GitHub Actionsには「Re-run failed jobs」ボタンがありますが、リランで通過したかどうかを記録するだけでもFlakyの把握が始まります。「2回連続リランが必要だったテスト」をリスト化するだけで、優先的に直すべきテストが見えてきます。

STEP 2:① Wait系Flakyの対策とは?

最も多く、比較的直しやすいFlakyです(SPAやvirtual DOM環境では難化する場合もあります)。「要素が見つからない」「クリックできない」は、要素の表示・操作可能状態を正しく待てていないことが原因です。

Selenium の場合

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

# 注:以下のコード例は driver が初期化済みであることを前提にしています
# 実際のテストでは pytest fixture(driver引数)またはsetUp()で生成してください

# ❌ Flakyになりやすい:time.sleepは固定待機
import time
time.sleep(3)
driver.find_element(By.ID, "submit").click()

# ❌ Flakyになりやすい:implicitly_waitとWebDriverWaitの混在
driver.implicitly_wait(10)  # グローバル設定
wait = WebDriverWait(driver, 5)  # 個別設定との干渉で予測不能に

# ✅ 推奨:WebDriverWaitで「クリック可能になるまで」明示的に待つ
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, "submit")))
element.click()

# ✅ 非同期でdisabled→enabledになるボタンへの対処
# ※ 以下は上記の wait = WebDriverWait(driver, 10) が定義済みであることが前提です
wait.until(lambda d: not d.find_element(By.ID, "submit").get_attribute("disabled"))

Playwright の場合

from playwright.sync_api import expect

# Playwrightは click() 実行前に visible / stable / enabled などを自動確認する
# auto-waiting 機能を持っています。ただしこれは「その瞬間の状態チェック」であり
# 「非同期状態変化を待つ」機能ではないため、expect()との併用が推奨されます

# ❌ auto-waitingに頼りすぎてFlakyになるケース
# (非同期でdisabled→enabledになるボタンはタイミング次第で落ちる)
page.locator("#submit").click()

# ✅ expect()で「enabled状態」を確認してからクリック
expect(page.locator("#submit")).to_be_enabled()
page.locator("#submit").click()

# ✅ APIレスポンス待ちが必要なコンテンツ
expect(page.locator(".search-results")).to_be_visible()
# → APIレスポンスが返ってリストが表示されるまで待つ
⚠️ Playwrightに移行してもFlakyが消えないケース:Playwrightのauto-waitingで改善しやすいのは主に「Wait設計起因」のFlakyです。データ競合系・外部依存系・設計起因系のFlakyは、ツールを変えても根本解決にはなりません。「Playwrightに移行したのになぜまだFlakyが出るのか」という問いへの答えは、本記事の5分類のどれに当てはまるかを確認することです。

STEP 3:② 環境依存系Flakyの対策とは?

「ローカルは通るがCIで落ちる」——viewportサイズ・CPU速度・タイムゾーン・ヘッドレスモード差異など、実行環境の違いが原因のFlakyです。

主な環境依存の原因と対策

原因症状対策
viewport差異CIでクリック不可--window-size=1920,1080 を固定
CI性能不足タイムアウトがCIのみ頻発CIのtimeoutをローカルより大きく設定
Dockerイメージ差異ブラウザバージョン不一致DockerイメージのChromeバージョンを固定
タイムゾーン日付・時刻を使うテストがCI環境で誤動作TZ=UTC をCI環境変数に設定
# ✅ Seleniumのヘッドレス設定(環境差異を最小化)
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless")
options.add_argument("--window-size=1920,1080")  # viewport固定
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")             # Linux CI環境での安定化
options.add_argument("--disable-dev-shm-usage")  # メモリ不足対策
driver = webdriver.Chrome(options=options)

# ✅ GitHub ActionsでのCIタイムアウト設定
# playwright.config.ts または pytest.ini で
# timeout = 30000  # ローカルより大きめに設定(ミリ秒)

STEP 4:③ データ競合系Flakyの対策とは?

「並列実行時だけ落ちる」「テストの順序を変えると落ちる」——これはテスト間でデータや状態を共有していることが原因です。修正難易度は高いですが、根本解決しないと永遠に再発します。

⚠️ データ競合の典型パターン:

  • テストデータの使いまわし:「テスト用ユーザー:test@example.com」を全テストが共有 → 一方が削除すると他方が落ちる
  • DBのクリーンアップ漏れ:テストAが作成したデータがテストBに影響する
  • グローバルな状態の共有:テストが実行順序に依存する(順序を変えると落ちる)
import pytest
import uuid

# ❌ Flakyになりやすい:テストデータを共有している
# test_A.py
def test_create_user():
    create_user("test@example.com")  # 固定のテストデータ

# test_B.py(並列実行するとtest_Aと競合する)
def test_delete_user():
    delete_user("test@example.com")  # test_Aと同じデータを操作

# ✅ 対策:テストごとにユニークなデータを生成する
@pytest.fixture
def unique_user():
    """テストごとにユニークなユーザーを作成し、テスト後に削除"""
    email = f"test_{uuid.uuid4().hex[:8]}@example.com"
    user = create_user(email)
    yield user
    # テスト後に必ずクリーンアップ
    try:
        delete_user(user.id)
    except Exception as e:
        # cleanup失敗はテスト本体の結果に影響させない
        # 実務では logger.warning(f"cleanup failed: {e}") などで記録することを推奨
        # ※ cleanup専用の簡略化例。通常のテストロジックでは広すぎるException捕捉は避ける
        pass

def test_update_user(unique_user):
    # ユニークなユーザーだけを操作するのでテスト間の競合なし
    update_user(unique_user.id, name="Updated")
💡 実務Tip:「テスト順序を変えると落ちるかどうか」を確認するには pytest --randomly-seed=random(pytest-randomly プラグイン)でテスト順序をシャッフルして実行してみましょう。順序依存のFlakyが即座に発見できます。

STEP 5:④ 外部依存系Flakyの対策とは?

外部APIのレスポンスが遅い・ネットワークが不安定・外部サービスがメンテナンス中——自分たちのコードではどうにもできない外部要因が原因のFlakyです。

import pytest
import requests
from unittest.mock import patch, MagicMock

# ❌ Flakyになりやすい:外部APIをそのまま叩く
def test_user_profile():
    response = requests.get("https://api.example.com/users/1")
    # 外部APIが遅い・落ちているとFlakyになる
    assert response.status_code == 200

# ✅ 対策①:外部APIをモック化する
@patch("requests.get")
def test_user_profile_mocked(mock_get):
    mock_get.return_value = MagicMock(
        status_code=200,
        json=lambda: {"id": 1, "name": "Test User"}
    )
    response = requests.get("https://api.example.com/users/1")
    assert response.status_code == 200
    # 外部APIに依存しないため安定

# ✅ 対策②:リトライを設定する(外部依存が避けられない場合)
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=4))
def call_external_api():
    return requests.get("https://api.example.com/users/1", timeout=(3, 10))
⚠️ リトライについて:リトライはあくまで「外部依存が避けられない場合の緩和策」です。リトライで解決したように見えるFlakyは、根本原因が残ったままなので、モック化できるものはモック化する方が先決です。

STEP 6:⑤ 設計起因系Flakyの対策とは?

「テスト順序を変えると落ちる」「特定のテストの後でだけ落ちる」——これはテスト設計に問題があるFlakyです。修正難易度は最も高く、設計の見直しが必要になります。

設計起因Flakyの主なパターン

  • テスト間の暗黙の前提条件依存:「テストAが実行された後の状態」を前提にテストBが書かれている
  • グローバルなfixture/状態の共有:状態変更を伴う scope="session" のfixtureは、意図せずテスト間で状態共有が発生しやすい(読み取り専用データやimmutableな設定値では安全に使えます)
  • 過度に長いE2Eテスト:「ログイン→検索→購入→決済」を1テストに詰め込みすぎて途中の失敗が他に波及
import pytest

# ❌ 設計起因Flaky:状態変更を伴うscope="session"で状態が引き継がれるケース
# ※ scope="session"自体が悪いのではなく、「状態変更を行うfixtureで使う」ことが問題
# (読み取り専用データ・immutableな設定値・ブラウザバイナリキャッシュ等では安全に使える)
@pytest.fixture(scope="session")
def logged_in_driver(driver):
    driver.get("/login")
    driver.find_element(By.ID, "email").send_keys("admin@example.com")
    driver.find_element(By.ID, "submit").click()
    yield driver
    # ログアウト処理なし → 次のテストに状態が残る

# ✅ 対策:テストごとにセットアップ・クリーンアップを完結させる
@pytest.fixture
def logged_in_driver(driver):
    """各テストで独立してログイン・ログアウトを行う"""
    driver.get("/login")
    driver.find_element(By.ID, "email").send_keys("admin@example.com")
    driver.find_element(By.ID, "submit").click()
    yield driver
    driver.get("/logout")  # ← 毎回クリーンアップ

STEP 7:直らないFlakyは隔離・削除を検討するとは?

「どうしても直らない」「直すコストが対策コストを大幅に上回る」Flakyは、隔離またはテスト自体の削除が正解のことがあります。「削除も正解」という判断ができるかどうかが、テストスイートを健全に保つカギです。なお削除によってカバレッジが低下し、そのシナリオのリグレッション検知が弱まるトレードオフがあります。削除する場合は、対象シナリオを別の手段(手動テスト・より小さな単体テスト)で担保することが条件です。

import pytest

# ✅ 方法①:@pytest.mark.skip で一時的に隔離
@pytest.mark.skip(reason="Flaky: 原因調査中 (2026-05-01 by Yoshi) #issue-123")
def test_payment_flow():
    pass

# ✅ 方法②:@pytest.mark.xfail で「失敗が期待される」マークをつける
@pytest.mark.xfail(reason="外部決済API不安定のため - #issue-456", strict=False)
def test_external_payment():
    pass

# ✅ 方法③:Flakyテスト専用のmarkを作り別CIステージで実行
# pytest.ini
# [pytest]
# markers =
#     flaky: 不安定なテスト(メインCIから除外)

@pytest.mark.flaky
def test_unstable_feature():
    pass

# メインCIでは除外して実行
# pytest -m "not flaky"
# 別ジョブで定期実行・監視
# pytest -m "flaky"
💡 判断基準:隔離・削除を検討すべきFlakyのサインは次の通りです。①3回以上対策しても再発する、②修正に1日以上かかる見込みで実行頻度が月1回以下、③そのテストがカバーしているシナリオは別の方法で品質担保できる——これら3つのうち2つ以上に当てはまれば、隔離か削除が合理的です。
⚠️ 隔離後の監視が重要:隔離したFlakyを「放置」にしてはいけません。「隔離したまま誰も直さず忘れられる」が最悪のパターンです。実務では以下の仕組みで可視化・継続監視することを推奨します。

  • Slack通知:@pytest.mark.flaky のテスト結果をSlackの専用チャンネルに自動通知
  • Flaky一覧の管理:隔離したテスト一覧・隔離日・担当者・issueリンクをスプレッドシートやJiraで管理
  • 週次レビュー:定期的に「隔離中テスト一覧」を確認し、3ヶ月以上放置されたものは削除を判断する

Flakyテストの再発防止:設計段階でやること

Flakyを直すだけでは不十分です。設計段階で再発を防ぐことが長期的に地獄から脱出し続けるための条件です。

設計ルール内容効果
テストの独立性各テストは前後のテストに依存しない🔴データ競合防止
ユニークデータテストデータは毎回ユニークに生成🔴データ競合防止
外部依存のモック化外部APIは原則モックで代替🔴外部依存防止
明示的な待機time.sleepは使わずexpect()/WebDriverWaitを使う🟢Wait系防止
環境の統一DockerイメージとCIのブラウザバージョンを固定🟡環境依存防止
定期的なFlaky計測週次でFlaky率を確認・5%超えたら即対処全般的な早期発見

FAQ

Q. 「とりあえずリラン」は絶対にNGですか?

NGではありませんが、リランは「応急処置」に過ぎません。問題はリランが「習慣」になった瞬間です。リランで通過したテストは記録に残し、「今週リランが必要だったテスト一覧」を定期的に確認して原因調査のインプットにしましょう。「記録しないリラン」が地獄の入り口です。

Q. PlaywrightはSeleniumよりFlakyが少ないですか?

Wait設計起因のFlakyは減ります。PlaywrightのLocator APIはactionability check(visible・stable・enabled等)を自動実行するため、SeleniumのimplicitWait/explicitWait混在問題は発生しにくいです。ただし、データ競合・外部依存・設計起因のFlakyはツールを変えても解決しません。「Playwrightに移行すればFlaky地獄から脱出できる」は半分正解です。

Q. pytest-retryなどの自動リトライプラグインを使うべきですか?

外部依存が避けられないテストへの緩和策としては有効です。ただし「自動リトライを入れるとFlakyが隠れてしまう」リスクがあります。リトライで通過したテストは必ずログに残し、根本原因の調査を継続することが条件です。自動リトライは「Flakyを直したことにしてくれるツール」ではありません。

Q. Flakyテストを削除するのは「テスト品質を下げること」になりませんか?

なりません。「誰も信頼しないFlakyテスト」は実質的にテスト品質に貢献していません。それどころか「CIが赤でも誰も気にしない」文化を作ることで、テストスイート全体への信頼を下げます。カバーしていたシナリオを別の方法(手動テスト・より小さな単体テスト)で担保した上で削除するのは合理的な判断です。

Q. Flakyテストの対応優先順位はどう決めますか?

①Flaky率が高いもの(5%以上)、②CIをブロックする頻度が高いもの、③修正難易度が低いもの(Wait系)——この3軸でスコアをつけて優先順位をつけるのが実務的です。難易度が高いデータ競合系・設計起因系は後回しにして、まず低難易度のWait系・環境依存系を片付けると「Quick Win」が得られてモチベーションが維持できます。

Flakyテスト地獄は「直し続ける体制」より「再発させない設計」の方が長期的なコストが低くなります。まず計測して可視化し、種類を特定して優先順位をつけて対処する——この順番で進めることが、地獄から永続的に脱出するための最短ルートです。

📋 この記事のまとめ

  • Flakyの本当の怖さは「テスト結果を誰も信頼しなくなること」——計測・可視化が最初のステップ
  • FlakyはWait系・環境依存系・データ競合系・外部依存系・設計起因系の5種類に分類できる
  • 修正難易度:Wait系<環境依存系<外部依存系<データ競合系≒設計起因系。低難易度から着手する
  • Wait系:time.sleepを廃止し、WebDriverWait/expect()で明示的に状態を確認する
  • データ競合系:テストごとにユニークデータを生成し、fixtureでクリーンアップを保証する
  • 直らないFlakyは隔離(@pytest.mark.skip)または削除も正解。「誰も信頼しないテスト」は害になる
  • 再発防止の設計ルール:独立性・ユニークデータ・外部依存モック化・環境統一・定期計測
タイトルとURLをコピーしました