「テストが落ちても本当のバグかどうかわからない」「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 などに取り込むと長期トレンドが見やすい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レスポンスが返ってリストが表示されるまで待つ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")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))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"- 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が生まれる原因】
- Selenium運用崩壊した話7選|Wait混在・ChromeDriver起因のFlaky
- Playwright導入で失敗した話7選|auto-waiting過信のFlaky
- 自動化しない方がいいテストケース7選|無理な自動化がFlakyを生む
【実装・設計】
- Playwright × pytest ベストプラクティス
- Selenium × pytest 実践ガイド|fixture・scope設計
- Page Object Model|メンテナンスコスト爆発を防ぐ設計パターン
【ロードマップ】
Flakyテスト地獄は「直し続ける体制」より「再発させない設計」の方が長期的なコストが低くなります。まず計測して可視化し、種類を特定して優先順位をつけて対処する——この順番で進めることが、地獄から永続的に脱出するための最短ルートです。
📋 この記事のまとめ
- Flakyの本当の怖さは「テスト結果を誰も信頼しなくなること」——計測・可視化が最初のステップ
- FlakyはWait系・環境依存系・データ競合系・外部依存系・設計起因系の5種類に分類できる
- 修正難易度:Wait系<環境依存系<外部依存系<データ競合系≒設計起因系。低難易度から着手する
- Wait系:time.sleepを廃止し、WebDriverWait/expect()で明示的に状態を確認する
- データ競合系:テストごとにユニークデータを生成し、fixtureでクリーンアップを保証する
- 直らないFlakyは隔離(@pytest.mark.skip)または削除も正解。「誰も信頼しないテスト」は害になる
- 再発防止の設計ルール:独立性・ユニークデータ・外部依存モック化・環境統一・定期計測
