「Playwrightのテストが突然失敗する…」「どこで落ちたのか調べ方がわからない…」そんな悩みを抱えていませんか?テストを書くことよりも、テストを安定させてデバッグすることのほうが実は難しいのです。この記事では、Playwrightのトレース機能・自動待機の仕組み・flakyテスト対策など、テストのデバッグと安定化に特化した実践的なノウハウを解説します。

Playwrightの基本的な使い方(インストール・スクショ・フォーム入力・pytest連携)については、入門記事「ブラウザを自動操作してユーザテストを行う|Playwrightとは?非エンジニアにも分かりやすく徹底解説」をあわせてご覧ください。本記事はその続きにあたる「安定化・デバッグ編」です。

なぜPlaywrightのテストは不安定になるのか?

browser test debugging
browser test debugging / Photo by Markus Spiske via Pexels

flakyテストとは

flakyテストとは、コードを変更していないのに「成功したり失敗したりする」不安定なテストのことです。ブラウザを使ったE2Eテストでは特に発生しやすく、放置すると「テスト結果を信頼できない」という最悪の状態に陥ります。

flakyテストの主な原因は以下のとおりです。

  • タイミングのずれ:ページの読み込みが遅く、要素がまだ存在しない状態でクリックしようとする
  • ネットワーク遅延:APIのレスポンスが遅れて表示が間に合わない
  • アニメーション:CSSアニメーション中に要素が操作できない状態になる
  • テスト間の副作用:前のテストが残したデータや状態が次のテストに影響する
  • 固定sleepの乱用time.sleep(2)のような固定待機は環境によって足りたり余ったりする

PlaywrightはなぜSeleniumより安定しやすいのか

Playwrightには自動待機(Auto-waiting)という仕組みが組み込まれています。Seleniumの暗黙的待機と異なり、Playwrightは要素が操作可能(visible・enabled・安定)になるまでの条件チェックを自動で行います。要素をクリックしようとしたとき、以下の条件がすべて満たされるまで自動的に待ってくれます。

  • 要素がDOMに存在する
  • 要素が表示されている(visible)
  • 要素が安定している(アニメーション中でない)
  • 要素が操作可能な状態である(disabled でない)
  • 要素がクリック可能な位置にある(他の要素に隠れていない)

この仕組みのおかげで、多くのケースではtime.sleep()を書かなくても安定して動作します。ただし「自動待機だけでは解決しない」ケースも存在するため、それが本記事のテーマです。

Playwrightのトレース機能:失敗原因を完全に可視化する

browser test debugging trace
browser test debugging / Photo by Jakub Zerdzicki via Pexels

トレース機能とは何か

Playwrightのトレース機能は、テスト実行中のすべての操作・ネットワーク通信・コンソールログ・スクリーンショットをひとつの.zipファイルに記録する機能です。失敗したテストがあったとき、このトレースファイルを専用ビューアで開くと、まるでタイムラインを巻き戻すようにどのステップで何が起きたかを確認できます。

CIサーバーでテストを実行して失敗したときに「手元では再現しない…」という悩みを抱えたことはありませんか?トレース機能はまさにその問題を解決するためにあります。

tracing.start / tracing.stop の基本的な使い方

pytestと組み合わせた基本的なトレース取得コードは以下のとおりです。

# test_trace_sample.py
import pytest


@pytest.fixture(scope='function')
def context_with_trace(browser):
    """テストごとにトレースを記録するフィクスチャ"""
    context = browser.new_context()

    # トレース開始:スクリーンショット・ソースも記録する
    context.tracing.start(
        screenshots=True,  # 各ステップのスクリーンショットを記録
        snapshots=True,    # DOMスナップショットを記録
        sources=True       # Pythonソースコードも記録
    )

    yield context

    # テスト終了後にトレースを保存
    context.tracing.stop(path='trace.zip')
    context.close()


def test_search_example(context_with_trace):
    """トレース付きのテスト例"""
    page = context_with_trace.new_page()
    page.goto('https://example.com')

    # ここでアサーションが失敗してもトレースが残る
    assert page.title() == 'Example Domain'

tracing.start()の主なオプションを整理しておきましょう。

オプション内容推奨設定
screenshots各アクション後のスクリーンショットTrue
snapshotsDOMのスナップショット(CSS込み)True
sources実行中のPythonソースコードTrue(デバッグ時)

失敗時だけトレースを保存するパターン(CI向け)

毎回トレースを保存するとファイルが増えすぎます。CIでは「テストが失敗したときだけ保存する」パターンが効率的です。

# conftest.py
import pytest


@pytest.fixture(scope='function')
def traced_context(browser, request):
    """失敗時のみトレースを保存するフィクスチャ"""
    context = browser.new_context()
    context.tracing.start(screenshots=True, snapshots=True, sources=True)

    yield context

    # テスト結果を確認して失敗時のみ保存
    if request.node.rep_call.failed:
        test_name = request.node.name
        context.tracing.stop(path=f'traces/{test_name}_trace.zip')
        print(f'\nトレース保存: traces/{test_name}_trace.zip')
    else:
        # 成功時はトレースを破棄(ファイルを作らない)
        context.tracing.stop()

    context.close()


# rep_callを使うためのhookも追加
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    setattr(item, f'rep_{rep.when}', rep)

show-traceビューアでトレースを確認する

保存したtrace.zipは、Playwrightが提供する専用ビューアで確認できます。ターミナルで以下のコマンドを実行するだけです。

# ローカルのトレースファイルを開く
playwright show-trace trace.zip

# テスト名つきのトレースを開く
playwright show-trace traces/test_search_example_trace.zip

ブラウザが起動してトレースビューアが表示され、以下の情報をタイムライン形式で確認できます。

  • アクションパネルgotoclickfillなどの各操作とかかった時間
  • スクリーンショット:各操作前後の画面状態
  • ネットワークタブ:リクエスト・レスポンスの詳細
  • コンソールログ:ブラウザのJavaScriptエラーやログ
  • DOMスナップショット:その時点のHTML構造(マウスオーバーで要素をハイライト)

特に「どのアクションでどんな画面状態だったか」をステップごとに確認できるDOMスナップショットは非常に強力です。「ボタンが隠れていてクリックできなかった」「テキストが期待値と違った」といった原因が一目でわかります。

失敗時のスクリーンショット・動画を自動で残す

テスト失敗時にスクリーンショットを自動保存する

トレースほど詳細でなくてよいケースでは、失敗時のスクリーンショットを自動保存するだけでもデバッグが大幅に楽になります。

# conftest.py に追加
import os
import pytest


@pytest.fixture(scope='function', autouse=True)
def auto_screenshot_on_failure(page: 'Page', request):
    """テスト失敗時に自動でスクリーンショットを保存"""
    yield

    # テストが失敗した場合のみスクリーンショットを保存
    if request.node.rep_call.failed:
        # screenshotsディレクトリがなければ作成
        os.makedirs('screenshots', exist_ok=True)

        test_name = request.node.name
        screenshot_path = f'screenshots/{test_name}.png'
        page.screenshot(path=screenshot_path, full_page=True)
        print(f'\nスクリーンショット保存: {screenshot_path}')

※このフィクスチャは、前節で紹介したpytest_runtest_makereportフックと同じconftest.pyに記述してください。request.node.rep_callはそのフックが設定する属性のため、フィクスチャ単体では動作しません。

テスト実行を動画で録画する

Playwrightはテスト全体を動画として録画することもできます。特に複雑なユーザー操作の流れをデバッグするときに便利です。

from playwright.sync_api import sync_playwright
import os

with sync_playwright() as p:
    browser = p.chromium.launch()

    # record_videoを指定してコンテキストを作成
    context = browser.new_context(
        record_video_dir='videos/',       # 保存先ディレクトリ
        record_video_size={'width': 1280, 'height': 720}  # 解像度
    )

    page = context.new_page()
    page.goto('https://example.com')
    page.click('a')  # 何らかの操作

    # contextを閉じると動画ファイルが確定する
    context.close()
    browser.close()

# videos/ ディレクトリにwebm形式で動画が保存される
print('videos/ ディレクトリに動画が保存されました')

pytestではbrowser_context_argsフィクスチャをオーバーライドすることで全テストに動画録画を適用できます。

# conftest.py
import pytest


@pytest.fixture(scope='session')
def browser_context_args(browser_context_args):
    """全テストで動画録画を有効化"""
    return {
        **browser_context_args,
        'record_video_dir': 'videos/',
        'record_video_size': {'width': 1280, 'height': 720}
    }

flakyテストの原因別対策集

対策①:expect()で明示的にアサーション待機する

Playwrightのexpect()は、条件が満たされるまで自動的にリトライするアサーション関数です。assert page.inner_text(...) == 'xxx'と書くより、圧倒的に安定します。

from playwright.sync_api import expect

# ❌ 不安定な書き方(その瞬間の値を確認するだけ)
assert page.inner_text('#result') == '検索完了'

# ✅ 安定した書き方(条件が満たされるまで最大5秒リトライ)
expect(page.locator('#result')).to_have_text('検索完了')

# タイムアウトを長くしたい場合
expect(page.locator('#result')).to_have_text(
    '検索完了',
    timeout=10000  # ミリ秒単位(10秒)
)

# 要素が表示されるまで待つ
expect(page.locator('.loading-spinner')).to_be_hidden()
expect(page.locator('#main-content')).to_be_visible()

対策②:wait_for_selector / wait_for_response で明示的に待機する

APIのレスポンスを待ってから操作したいケースでは、wait_for_response()が有効です。

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto('https://example.com')

    # ❌ 固定sleep(環境によって足りないことがある)
    # import time
    # time.sleep(3)

    # ✅ 特定のAPIレスポンスを待ってから次の操作へ
    with page.expect_response('**/api/search**') as response_info:
        page.click('#search-button')
    response = response_info.value
    assert response.status == 200

    # ✅ 特定のセレクタが表示されるまで待機
    page.wait_for_selector('#search-results', state='visible')

    # ✅ ネットワークがアイドル状態になるまで待機(重いSPAで有効)
    page.wait_for_load_state('networkidle')

    browser.close()

対策③:ロケーターを安定したものに変える

テストが不安定になる原因のひとつが不安定なロケーター(要素の指定方法)です。XPathや複雑なCSSセレクタは、HTML構造が少し変わっただけで壊れます。

# ❌ 壊れやすいロケーターの例
page.click('div > div:nth-child(2) > button')  # 構造依存
page.click('xpath=//div[@class="container"]/button[1]')  # 位置依存

# ✅ 安定したロケーターの例(推奨順)

# 1. テスト用のdata属性を使う(最も安定)
page.click('[data-testid="submit-button"]')

# 2. ロールとテキストを組み合わせる(セマンティックで安定)
page.get_by_role('button', name='送信する').click()

# 3. ラベルテキストで入力フォームを特定
page.get_by_label('メールアドレス').fill('test@example.com')

# 4. プレースホルダーで特定
page.get_by_placeholder('例:example@mail.com').fill('test@example.com')

# 5. テキストで特定
page.get_by_text('ログイン').click()

特にdata-testid属性をHTML側に仕込んでおく方法は、フロントエンドの実装が変わってもテストが壊れにくく、現場でも広く採用されています。

対策④:リトライ設定でflakyテストをカバーする

どうしても不安定なテストには、pytestのリトライ機能を組み合わせることで、一時的なネットワーク遅延などに対応できます。

# pytest-rerunfailuresをインストール
# pip install pytest-rerunfailures

# コマンドラインで最大2回リトライ
# pytest --reruns 2 --reruns-delay 1

# 特定のテストだけリトライを設定する場合
import pytest

@pytest.mark.flaky(reruns=2, reruns_delay=1)
def test_flaky_network_dependent():
    """ネットワーク依存のためリトライを許可"""
    pass

ただし、リトライはあくまで一時的な対策です。根本的な原因(不安定なロケーター・固定sleep・不適切な待機)を解消することを優先してください。

デバッグ効率を上げるPlaywrightの実践テクニック

コンソールログとエラーを収集する

ブラウザのJavaScriptエラーがE2Eテストの失敗原因になっていることがあります。以下のコードでコンソール出力をすべてPython側に収集できます。

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()

    # コンソールログをすべてキャプチャ
    console_messages = []
    page.on('console', lambda msg: console_messages.append({
        'type': msg.type,
        'text': msg.text
    }))

    # JavaScriptエラーをキャプチャ
    js_errors = []
    page.on('pageerror', lambda error: js_errors.append(str(error)))

    page.goto('https://example.com')
    # テスト操作...

    # テスト終了後に確認
    if js_errors:
        print('=== JavaScriptエラー ===')
        for err in js_errors:
            print(err)

    errors = [m for m in console_messages if m['type'] == 'error']
    if errors:
        print('=== コンソールエラー ===')
        for e in errors:
            print(e['text'])

    browser.close()

PWDEBUG:ステップ実行モードで動きを確認する

Playwrightにはインタラクティブデバッグモードが用意されています。環境変数PWDEBUG=1を設定してテストを実行すると、Playwrightインスペクターが起動し、1ステップずつ手動で進められます。

# Windows(コマンドプロンプト)
set PWDEBUG=1
pytest test_sample.py -s

# Mac / Linux
PWDEBUG=1 pytest test_sample.py -s

インスペクターが起動すると次のことができます。

  • 「Step Over」ボタンで1操作ずつ確認しながら進める
  • ブラウザ上の要素をクリックして、そのロケーターを自動生成してくれる
  • Pythonコードの各行が実行されるたびに、ブラウザの状態が視覚的に確認できる

デバッグに役立つページ情報の取得

テストが失敗したときに現在のページ状態をログに出力しておくと、原因特定が速くなります。

def debug_page_state(page):
    """デバッグ用:現在のページ状態を出力するヘルパー関数"""
    print(f'現在のURL: {page.url}')
    print(f'ページタイトル: {page.title()}')

    # ページ内のすべてのエラーメッセージを収集
    error_elements = page.locator('[class*="error"], [class*="alert"]').all()
    if error_elements:
        print('=== 画面上のエラー要素 ===')
        for el in error_elements:
            if el.is_visible():
                print(f'  - {el.inner_text()}')

    # 現在フォーカスされている要素を確認
    focused = page.evaluate('document.activeElement.tagName')
    print(f'フォーカス中の要素: {focused}')

まとめ:Playwrightデバッグ・安定化のチェックリスト

本記事で紹介したPlaywrightのデバッグ・安定化テクニックをまとめます。

問題対策
失敗原因がわからないtracing.start/stopでトレースを記録し、show-traceビューアで確認
CIでだけ失敗する失敗時のみトレース保存+スクリーンショット自動保存
タイミング起因のflakyテストexpect()によるアサーション待機・wait_for_response()の活用
ロケーターが壊れやすいdata-testid属性・get_by_role()・get_by_label()に切り替える
JSエラーが原因かもしれないpage.on(‘pageerror’)でJSエラーをキャプチャ
どのステップで失敗するか追いたいPWDEBUG=1でインタラクティブデバッグモードを使う

E2Eテストの安定化は一朝一夕では達成できませんが、トレース機能を使って原因を正確に特定し、適切な待機戦略とロケーターに置き換えていくことで着実に改善できます。

Playwrightの基本的な使い方・インストール・pytest連携から学びたい方は、ブラウザを自動操作してユーザテストを行う|Playwrightとは?非エンジニアにも分かりやすく徹底解説をあわせてご覧ください。基礎から応用まで一気に理解できます。

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る
徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

もしも

徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

電子工作とプログラミングを同時に学べる

Amazonで見る
ELEGOO Arduino用UNO R3スターターキット レベルアップ チュートリアル付 mega2560 r3 nanoと互換 [並行輸入品]

もしも

ELEGOO Arduino用UNO R3スターターキット レベルアップ チュートリアル付 mega2560 r3 nanoと互換 [並行輸入品]

チュートリアル付きのArduino入門セット

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。