Playwright Flaky Test完全ガイド|原因の切り分けとAuto-waiting・Locator・Trace Viewerでの解決策

Playwright は Auto-waiting 機能により Selenium と比べて Flaky Test(不安定なテスト)が起きにくいと言われますが、それでもゼロにはなりません。「ローカルでは通るのにCIだけ落ちる」「たまに失敗するが再実行すると通る」——こうした症状に悩むQAエンジニアは少なくありません。この記事では、Playwright特有のFlaky Testの原因を切り分け、Auto-waiting・Locator設計・Trace Viewerを使った解決策を実務目線で解説します。

Flaky Test を「たまたま」で片付けてリトライだけに頼ると、根本原因が積み重なってテストスイート全体の信頼性が下がっていきます。原因を切り分けて直す習慣が、長期的にはリトライより効果的です。

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

  • Playwright のテストが「たまに落ちる」現象に悩んでいるQAエンジニア・開発者
  • CI では失敗するのにローカルでは再現しない経験がある方
  • Auto-waiting の仕組みをきちんと理解していない方
  • Trace Viewer を使った原因調査の流れを知りたい方

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

  • Playwright特有のFlaky Test原因を4パターンに分類して理解できる
  • Auto-waiting・Locator・タイムアウト設定の見直しポイントがわかる
  • Trace Viewer を使った原因調査の具体的な手順が実装できる

👤 この記事を書いた人

QAエンジニア・テスト自動化エンジニアとして15年以上の実務経験を持つ Yoshi が執筆。Selenium から Playwright への移行プロジェクトも経験し、Flaky Test の切り分けと対策を実務で継続的に行っています。

📌 結論

  • Playwright の Flaky Test は「Auto-waiting任せ」「不安定なLocator」「CI特有の環境差」が主な原因になりやすい
  • リトライ(pytest-rerunfailures)は応急処置であり、根本原因の特定・修正が優先
  • Trace Viewer を使えば、CIでしか再現しない失敗も視覚的に原因調査できる

Playwright は Selenium に比べて Flaky Test が起きにくい設計になっていますが、次のような症状は珍しくありません。

  • ローカルでは毎回通るのに、CI環境だけ時々失敗する
  • 同じテストを再実行すると通ってしまい、原因を特定できない
  • アニメーションが絡む画面でクリックが失敗する
  • 並列実行を有効にすると失敗率が上がる

この記事では、これらの症状を引き起こす原因を切り分け、それぞれの解決策を順番に見ていきます。

Flaky Testとは?なぜPlaywrightでも起きるのか

Flaky Testとは、コードやテスト対象の仕様に変更がないにもかかわらず、実行するたびに成功したり失敗したりするテストのことです。

Playwright は Selenium と比較して Auto-waiting(要素が操作可能になるまで自動的に待機する仕組み)が組み込まれているため、明示的な待機処理を書き忘れることによる Flaky Test は大幅に減ります。ただし、これは「待機処理を考えなくていい」という意味ではありません。Auto-waiting がカバーする範囲とカバーしない範囲を理解していないと、依然として Flaky Test は発生します。

💡 Selenium との違い:Selenium では「要素は見つかったが、まだクリック可能な状態ではない」というケースで明示的な wait(WebDriverWait)を書く必要がありました。Playwright はこの種の待機を自動化していますが、ネットワーク応答待ちやアニメーション完了待ちなど、Auto-waiting だけでは解決しない要因は依然として存在します。

Playwright特有のFlaky Test原因 4パターン

実務でよく遭遇する原因を4つに分類しました。まずはどのパターンに該当するかを切り分けることが解決の第一歩です。

① Auto-waitingの誤解

症状:要素は存在するがクリックできない・値が反映されない

対策:Actionability checks を理解する

② 不安定なLocator

症状:CSSクラス変更やDOM構造変化で要素が見つからなくなる

対策:role・test-idベースのLocatorに変更

③ ネットワーク・API待ち不足

症状:API応答が遅い時だけ失敗する(特にCI環境)

対策:expect()の自動リトライアサーション活用

④ CI環境特有の要因

症状:ローカルでは再現せずCIだけ失敗する

対策:並列実行設計・リソース制約の見直し

① Auto-waitingを正しく理解する

Playwright は要素を操作する前に、以下の Actionability checks(操作可能性チェック)を自動的に行います。

チェック項目内容
Attached要素がDOMに存在しているか
Visible要素がユーザーから見える状態か(display:none・visibility:hidden・サイズ0など可視性に関わる要因を含めて判定)
Stableアニメーション中でなく、位置が安定しているか
Receives Events他の要素に覆われておらず、クリックを受け取れるか
Enableddisabled属性が付いていないか
Editable(fill()使用時)入力可能な状態か

つまり Playwright がカバーしているのは「要素が操作可能な状態になるまで」の待機であり、以下のようなケースは Auto-waiting の範囲外です。

  • ボタンをクリックした後、非同期でAPIが呼ばれてデータが反映されるまでの待機
  • 要素は表示されているが、内部の値がまだ更新されていない状態の判定
  • ページ遷移後、目的の要素が読み込まれるまでの待機(これは通常自動でカバーされるが、SPAの複雑な遷移では注意が必要)

❌ よくある誤り:sleepで無理やり待つ

import time
from playwright.sync_api import Page

def test_add_to_cart_bad(page: Page) -> None:
    page.goto("/products/1")
    page.get_by_role("button", name="カートに追加").click()

    # ❌ 固定時間の sleep はCI環境の遅延に弱く、Flaky Testの温床になる
    time.sleep(2)

    assert page.get_by_text("カートに追加しました").is_visible()

✅ 改善例:expect() の自動リトライアサーションを使う

from playwright.sync_api import Page, expect

def test_add_to_cart_good(page: Page) -> None:
    page.goto("/products/1")
    page.get_by_role("button", name="カートに追加").click()

    # ✅ 条件を満たすまで自動的にリトライしながら確認する
    expect(page.get_by_text("カートに追加しました")).to_be_visible()
🔍 実務あるある:Selenium からの移行プロジェクトでは、time.sleep() が Playwright のコードにそのまま残ってしまうケースをよく見ます。Playwright には固定 sleep がほぼ不要な設計になっているため、レビューで見つけたら expect() への置き換えを検討する価値があります。

② 安定したLocatorを選ぶ

CSS セレクタや XPath は、デザイン変更やクラス名のリファクタリングだけで簡単に壊れます。Playwright は ユーザー視点のLocator(role・label・text)を推奨しており、これらは実装の詳細変更に強い傾向があります。

Locator安定性使いどころ
get_by_role()◎ 高いボタン・リンクなどアクセシビリティロールを持つ要素(第一候補)
get_by_label()◎ 高いフォーム入力欄
get_by_test_id()◎ 高いrole・labelで表現しにくい要素(開発側とdata-testidの運用合意が前提)
get_by_placeholder()△ 中程度プレースホルダー付きの入力欄(label がない場合の代替。文言変更に弱い点に注意)
get_by_text()△ 中程度静的なテキスト(多言語対応時は変更に弱い点に注意)
CSS / XPath✕ 低い他の方法で表現できない場合の最終手段

❌ 不安定な例:CSSクラスに依存

# ❌ CSSクラス名はスタイル変更のたびに壊れるリスクがある
page.locator(".btn.btn-primary.submit-btn-v2").click()

✅ 安定した例:role ベース

# ✅ ユーザーが見る役割・名前に基づくため、内部実装の変更に強い
page.get_by_role("button", name="送信").click()
💡 実務Tip:role や label で表現しづらい複雑なUI(グリッド内の特定セルなど)では get_by_test_id() が現実的な選択肢になります。ただし data-testid の付与・維持は開発チームとの合意が前提になるため、導入時にルールを決めておくとスムーズです。

③ タイムアウト設定を見直す

Playwright には複数の階層のタイムアウトが存在します。それぞれの役割を理解しておくと、原因調査がしやすくなります。

種類対象デフォルト(目安)
アクションタイムアウトclick()・fill()などの個別操作30秒
expect() タイムアウトアサーションの自動リトライアクションタイムアウトとは別に設定される場合があります
テスト全体のタイムアウト1テストケース全体の実行時間プロジェクト設定による
⚠️ 注意:タイムアウトの具体的なデフォルト値は Playwright のバージョンや使用しているテストランナー(pytest-playwright など)によって変わる場合があります。プロジェクトで使用しているバージョンの公式ドキュメントを確認することを推奨します。

タイムアウトを個別に指定する例

from playwright.sync_api import Page, expect

def test_slow_api_response(page: Page) -> None:
    page.goto("/reports")
    page.get_by_role("button", name="レポート生成").click()

    # API応答が遅いことがわかっている場合、個別にタイムアウトを延長する
    # ms(ミリ秒)指定であることに注意
    expect(page.get_by_text("生成完了")).to_be_visible(timeout=60_000)
🔍 実務あるある:「CIだけタイムアウトする」という相談の多くは、CI環境のCPU・メモリリソースがローカルより制約されていて、処理に時間がかかっているケースです。タイムアウトを一律で伸ばすのではなく、遅くなりやすい処理(大量データの描画・重いAPI呼び出しなど)を特定して個別に調整する方が、テスト全体の実行時間を無駄に伸ばさずに済みます。

④ pytest-rerunfailures でリトライする(応急処置として)

根本原因の特定・修正が理想ですが、リリース直前などですぐに対応しきれない場合、リトライで一時的に緩和する選択肢もあります。ただし、これは pytest 標準機能ではなく pytest-rerunfailures というプラグインに依存する点に注意してください。

インストール

pip install pytest-rerunfailures

使い方

# CLIオプションで全体に適用する場合
# 失敗したテストを最大3回まで再実行し、再実行の間は2秒待機する
pytest --reruns 3 --reruns-delay 2

特定のテストにだけ適用したい場合は、@pytest.mark.flaky をデコレータとして関数に付与します。

import pytest
from playwright.sync_api import Page

@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_login(page: Page) -> None:
    page.goto("/login")
    # ... テスト処理 ...
⚠️ リトライを使う際の注意点

  • リトライは症状を隠すだけで、根本原因は解決しません。乱用するとテストスイート全体の信頼性が下がります
  • 「どのテストが何回リトライされたか」を記録・監視する仕組みがないと、問題の蓄積に気づけなくなります
  • 本当に不安定な原因(Auto-waiting・Locator設計など)は、リトライで隠さず優先的に修正することをおすすめします

⑤ Trace Viewer で原因を特定する

「CIだけ失敗する」「たまに失敗する」といった再現性の低い Flaky Test の原因調査には、Trace Viewer が特に有効です。テスト実行時の操作・スクリーンショット・ネットワークログ・コンソールログを1ファイルにまとめて記録できます。

※ 以下は pytest-playwright プラグインを利用している場合の例です。

失敗時のみTraceを保存する

# 失敗時のみ Trace を保存する(pytest-playwright プラグイン使用時)
pytest --tracing retain-on-failure

# 保存された trace.zip をブラウザで確認する
playwright show-trace trace.zip
⚠️ 注意:上記は pytest-playwright プラグイン(pip install pytest-playwright)を使用している場合の例です。標準の pytest コマンドには --tracing オプションはありません。実際の動作は利用しているプラグインのバージョンに依存するため、最新情報は Playwright 公式ドキュメントも合わせて確認してください。

Trace Viewer で確認すべきポイント

確認箇所わかること
各ステップのスクリーンショット失敗直前の画面状態・要素が想定通り表示されていたか
Actionability のログどのチェック(Visible・Stableなど)で待機が発生していたか
ネットワークタイムラインAPI応答が遅延していないか・想定外のリクエストが発生していないか
コンソールログJSエラーが操作を妨げていないか
🔍 実務あるある:Trace Viewer で確認すると、「クリックはできていたが、その後のAPI応答待ちが足りずアサーションが早すぎた」というパターンが非常に多いです。Actionability checks を通過していても、アプリケーション側の非同期処理が終わっていないケースは Auto-waiting の範囲外なので、原因調査では両方を切り分けて見る必要があります。

CI/CD特有の原因と対策

「ローカルでは再現しないがCIだけ失敗する」場合、CI環境特有の要因を疑う必要があります。GitHub Actions の設定自体が原因になっていないかも合わせて確認すると効率的です(詳しくはGitHub Actions で Playwright を CI/CD に組み込むガイドを参照)。

要因説明対策
リソース制約CIランナーのCPU・メモリがローカルより少ない場合が多く、処理が遅延しやすい重い処理のタイムアウトを個別に延長・並列数を調整
並列実行によるデータ競合複数テストが同じDBデータ・アカウントを共有していると干渉が起きる各テストが独立したテストデータを使う設計にする
Headless特有の描画差異フォント・GPUレンダリングの違いでレイアウトがわずかに異なる場合があるレイアウトに依存しないLocator・アサーションを使う
ネットワーク遅延CI環境から外部APIへのアクセスがローカルより遅いことがあるexpect() の自動リトライを活用し、固定待機に頼らない

実務でよくある失敗パターン

① 「動かないから」とテストをリトライ設定でごまかす

失敗するたびに reruns の回数を増やして対応していると、根本原因が積み重なり、いずれリトライしても通らないレベルまで悪化します。リトライ回数を増やす前に、失敗頻度が高いテストから原因調査を優先しましょう。

② Auto-waitingとexpect()の自動リトライを混同する

「Playwrightは自動で待ってくれる」という理解のまま、通常の assert(Pythonの標準アサーション)で状態を確認してしまうケースです。assert は一度きりの評価で自動リトライされません。状態が変化するのを待ちながら確認したい場合は、必ず expect() を使いましょう。

assert(標準)expect()(Playwright)
評価タイミング実行された瞬間の1回だけタイムアウトまで条件を満たすか繰り返し評価
非同期な状態変化変化前に評価してFailしやすい変化を待ってから判定するため安定しやすい
向いている用途変化しない値の確認(計算結果など)画面表示・API反映など変化を伴う確認

③ 並列実行を有効にした途端にテストが不安定になる

テストが同じユーザーアカウント・同じDBレコードを共有している場合、並列実行でデータ競合が発生します。並列化する前に、各テストが独立したデータで動作する設計になっているか確認しましょう。

④ Flaky Testを放置してSkipにする

「このテストはよく落ちるから」と @pytest.mark.skip をつけて無効化してしまうと、そのテストが本来検出すべきバグを見逃す状態が続きます。Skipにする前に、Trace Viewer で一度原因を調査する習慣をつけることをおすすめします。

FAQ

Q. Playwright を使えばFlaky Testは完全になくなりますか?

完全にはなくなりません。Auto-waitingにより「待機処理の書き忘れ」による Flaky Test は大幅に減りますが、CI環境特有のリソース制約・並列実行のデータ競合・アプリケーション側の非同期処理待ちなど、Auto-waitingの範囲外の原因は依然として発生します。

Q. リトライ(pytest-rerunfailures)は使わない方がいいですか?

完全に禁止する必要はありませんが、根本原因の修正を後回しにする理由にしないことが重要です。応急処置として使う場合も、どのテストが何回リトライされているかを記録・監視し、頻繁にリトライが発生するテストは優先的に調査・修正する運用がおすすめです。

Q. expect() と Python標準の assert は何が違いますか?

assert はその時点の状態を一度だけ評価します。状態がまだ変化中の場合、正しい結果が出る前に評価してしまい失敗することがあります。expect() は指定したタイムアウトの間、条件を満たすまで自動的に再評価(リトライ)してくれるため、非同期に変化する状態の確認に適しています。

Q. ローカルでは再現しないCI限定のFlaky Testはどう調査すればいいですか?

まず Trace Viewer をCI環境で失敗時に保存する設定(--tracing retain-on-failure)にして、artifacts としてアップロードする仕組みを作ることをおすすめします。ローカルで再現しない原因はリソース制約やタイミングの違いであることが多いため、実際にCIで失敗した際のTraceを確認するのが最も確実です。

Q. アニメーションが原因のFlaky Testはどう対処すればいいですか?

Playwright の Actionability checks には「Stable」(位置が安定しているか)の判定が含まれるため、多くのアニメーションは自動的に待機されます。それでも問題が起きる場合は、テスト環境でアニメーションを無効化する設定(CSSやアプリケーション側の設定)を検討する方法もあります。ただし、アニメーション自体の動作確認が目的のテストでは適用しない判断も必要です。

Q. 並列実行とFlaky Testの関係を教えてください

並列実行そのものがFlaky Testを引き起こすわけではなく、テスト間でデータやリソースを共有している設計が原因になることがほとんどです。各テストが独立したユーザー・データを使うよう設計されていれば、並列実行はむしろ実行時間短縮のメリットを安全に享受できます。

📋 まとめ

  • Auto-waitingは要素が操作可能になるまでの待機を自動化するが、非同期のAPI応答待ちなどは範囲外
  • role・test-id ベースのLocatorを使うことで、CSSクラス変更などの実装詳細変更に強くなる
  • Trace Viewerを使えば、CIでしか再現しないFlaky Testも視覚的に原因調査できる
  • リトライは応急処置であり、根本原因の特定・修正を優先することが長期的な安定性につながる

🚀 まずやること3ステップ

  1. 失敗時に Trace を保存する設定(--tracing retain-on-failure)を有効にする
  2. 頻繁に落ちるテストから Trace を確認し、原因を4パターンに当てはめて切り分ける
  3. 該当する対策(Locator見直し・expect()への置き換え・タイムアウト調整)を1つずつ適用する

Flaky Test は「たまたま」で終わらせず、原因を切り分けて一つずつ潰していくことで、テストスイート全体の信頼性が積み上がっていきます。まずは Trace Viewer を有効にして、失敗時の記録を残す仕組みから始めてみてください。

タイトルとURLをコピーしました