E2Eテストで死なないための「泥臭い」生存戦略:Playwrightを極める

E2Eテストで死なないための「泥臭い」生存戦略:Playwrightを極める

「E2Eテストは書くたびに壊れる。CIは常に真っ赤で、修正に追われて機能開発が止まる」。そんな地獄を経験した奴は多いはずだ。俺も以前はそうだった。朝起きてSlackを開くのが怖くなるような、フラキーテストの墓場を管理していた。

だが、結論から言うと、E2Eテストが壊れるのはお前の設計が悪いからだ。 ツールやフレームワークのせいにするな。今日は、俺が血反吐を吐きながら学んだ、Playwrightで「メンテナンス地獄」を回避するための現実的な設計論を叩き込む。


1. セレクタの「聖域」を守れ:CSSクラスという名の毒

`div.container > div:nth-child(2) > button` なんてセレクタを書く奴は、自分で時限爆弾を仕掛けているのと同じだ。マークアップが少し変われば即座に沈没する。

なぜ `getByRole` なのか

Playwrightの `getByRole` は、単なるセレクタではない。これは「ブラウザがどう要素を認識しているか」というアクセシビリティツリーに基づいている。つまり、DOM構造という「皮膚」が変わっても、役割という「骨格」が残っていればテストは落ちない。

`data-testid` との付き合い方

「じゃあ全部 `data-testid` にすればいいのか?」という議論があるが、俺は半分正解で半分間違いだと思う。
  • セマンティクス(getByRole): ユーザーが触るボタン、リンク、見出し。これらは絶対に `getByRole` で書くべきだ。
  • data-testid: 複雑なカスタムコンポーネントや、動的に生成されるリストの識別子など、セマンティクスで表現しきれない「実装の詳細」に対してのみ使う。
「CSSクラスで指定する=デザインに依存する」 という悪習を今すぐ捨てろ。

2. 認証の苦悩:セッション管理の泥臭い現実

`auth.json` で `storageState` を保存するのは基本だが、初心者はここで思考停止する。「セッションが切れたらどうする?」、「権限の違うユーザーが混ざったらどうする?」というエッジケースを無視するな。

実務レベルでは、「認証状態の再生成ロジック」をテストセットアップに組み込むのが正解だ。

# 認証状態を管理するフィクスチャの例
@pytest.fixture(scope="session")
def authenticated_page(browser):
    context = browser.new_context()
    page = context.new_page()
    
    # セッションファイルが存在し、有効期限内かチェック
    if os.path.exists("auth.json") and is_session_valid("auth.json"):
        context = browser.new_context(storage_state="auth.json")
    else:
        # なければログインして再生成
        perform_login(page)
        context.storage_state(path="auth.json")
        
    return context.new_page()

ここで重要なのは、「テストが落ちた原因が認証切れなのか、機能のバグなのか」を切り分けることだ。セッションの管理を怠ると、テストが「認証エラー」で落ちるのか「アプリケーションのバグ」で落ちるのか分からなくなり、デバッグの時間が無駄に溶ける。


3. Playwrightの標準ランナーを使え:Jestとの不毛な戦い

かつて俺は、Jestの既存エコシステム(モックやカバレッジ計測)に執着して、PlaywrightをJestの中で無理やり動かそうとした。結果はどうなったか? 並列実行の制御で地獄を見た。

Playwrightの標準テストランナーは、それ自体が最強のE2E環境だ。ブラウザの起動、コンテキストの分離、トレースビューアによる失敗時の解析。これらを捨ててまでJestにこだわる理由は、もはや「技術的負債」でしかない。

比較項目Playwright標準ランナーJest + Playwright連携
並列実行圧倒的に高速・安定リソース競合で不安定
デバッグTrace Viewerで完全再現コンソールログ頼み
自動待機完璧に統合済み手動設定が必要な場合あり

既存のJest環境を捨てられないなら、「ユニットテストとE2Eは完全に分離して実行する」のが唯一の正解だ。混ぜるな、腐る。


4. 「待機」の誘惑に勝つ

`page.waitForTimeout(5000)` を書くのは、プログラミングを諦めた敗北者の証だ。

もしテストが不安定で待機が必要なら、それは「テストの書き方が悪い」のではなく「アプリケーションの非同期処理がブラックボックスになっている」可能性が高い。

  • APIレスポンスを待つ: `page.wait_for_response(lambda r: "/api/submit" in r.url)`
  • DOMの更新を待つ: `expect(locator).to_be_visible()`
これらを使いこなせない奴は、いつまで経っても「たまに落ちるテスト」を直し続けることになる。

最後に:E2Eは「プロダクトの一部」だ

E2Eテストを「品質保証のための外付けパーツ」だと思っているなら、今すぐ考えを改めろ。テストコードは、プロダクトの仕様を最も正確に記述した「ドキュメント」だ。

俺がかつて、ある大規模リファクタリングを完遂できたのは、Playwrightのテストが「何が正解で、何が間違いか」を教えてくれたからだ。テストが壊れるのは、お前のコードが壊れている証拠だ。

泥臭い努力を避けようとするな。テストコードをプロダクトコードと同じ熱量で磨き上げろ。それが、最強のエンジニアへの唯一の近道だ。