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: 複雑なカスタムコンポーネントや、動的に生成されるリストの識別子など、セマンティクスで表現しきれない「実装の詳細」に対してのみ使う。
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のテストが「何が正解で、何が間違いか」を教えてくれたからだ。テストが壊れるのは、お前のコードが壊れている証拠だ。
泥臭い努力を避けようとするな。テストコードをプロダクトコードと同じ熱量で磨き上げろ。それが、最強のエンジニアへの唯一の近道だ。