E2Eテストで深夜に泣かないための設計思想:Playwright地獄からの脱出記
「CIが通った!」と歓喜のガッツポーズをしてPCを閉じた翌朝、Slackに届いた無慈悲なアラートの山。心臓が止まるかと思った。E2Eテストの自動化なんて「Playwrightなら爆速だし、コード書くだけでしょ?」と高を括っていた、あの頃の自分を殴り飛ばしたい。
結局、自動テストは「書くこと」よりも「壊れないように維持すること」の方が100倍難しい。これは、5年間泥水をすすりながらE2Eテストと格闘し、ようやく辿り着いた「深夜に叩き起こされないための」生存戦略だ。
1. 認証地獄からの解放:テストの「前提」をハックせよ
ログインテストを全ケースで実行するなんて、正気の沙汰じゃない。初期の私は、テストのたびに `page.fill()` でメールアドレスとパスワードを打ち込んでいた。結果、テストが100個を超えた頃には実行時間が20分を超え、挙句の果てにはログインAPIへの過負荷で開発環境のDBが昇天した。
解決策は、「認証状態の使い回し」だ。Playwrightの `storageState` を使い、ログイン後のCookieを保存して「ログイン済み」の状態でテストを開始する。
# auth.setup.py
from playwright.sync_api import sync_playwright
def run():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://myapp.com/login")
page.fill('input[name="email"]', "[email protected]")
page.fill('input[name="password"]', "password")
page.click('button[type="submit"]')
# ログイン後の状態をファイルに保存
page.context.storage_state(path="auth.json")
browser.close()
if __name__ == "__main__":
run()
これをテスト実行前に一度だけ走らせ、`pytest` の設定で `storage_state="auth.json"` を読み込ませる。これだけで、ログイン処理の待機時間はゼロになる。「ログインテストはログイン機能のテストだけでいい」。この割り切りが、テストスイートを劇的に軽量化する。
2. 非同期の「待ち」を攻略する:`waitForTimeout` は悪魔の囁き
動的コンテンツの読み込みで詰まると、つい `page.wait_for_timeout(5000)` を書きたくなる。だが、それは「負債」を積み上げているのと同じだ。ネットワークが遅い日は落ちるし、速い日は無駄に待つ。
Playwrightが提供する Web-Firstアサーション を信じろ。これは要素の状態を監視し、出現した瞬間にテストを進める。
# 悪い例:ハードコード(絶対やるな)
page.wait_for_timeout(3000)
assert page.locator('.result-item').is_visible()
# 良い例:Web-Firstアサーション
# 要素が揃うまで内部で自動リトライ(デフォルト5秒)
expect(page.locator('.result-item')).to_have_count(5, timeout=10000)
もし「なぜこのアサーションがベストなのか」と聞かれたら、答えはシンプルだ。「テストがコードの実行速度に依存せず、DOMの変化に依存するから」。これこそが、Flakyな(不安定な)テストを撲滅する唯一の道だ。
3. iframeという深淵:なぜ要素が見つからないのか
Playwrightで最も沼るのが `iframe` だ。通常の `page.locator()` で探しても、どれだけセレクターを精査しても `TimeoutError` で落ちる。なぜなら、iframeの中身はメインのDOMツリーとは別の世界だからだ。
`frame_locator` を使って、明確に「どのフレームの中を探すか」を宣言する必要がある。
# iframe内の要素を正しく特定する
frame = page.frame_locator('iframe[name="payment-form"]')
frame.locator('input[name="card-number"]').fill('1234-5678-9012-3456')
もし、iframeが動的に生成されるようなクソ仕様なら、`page.frame_locator` を変数に持たせ、再利用する設計にすること。セレクターの解像度を上げるだけでなく、「どこが親なのか」を構造的に定義するだけで、テストの安定度は激変する。
4. CI環境でだけ落ちる問題:デバッグの指針
ローカルでは完璧なのに、GitHub Actionsでだけ落ちる。この原因の8割は「環境の差異」だ。
1. ビューポートの固定: ローカルとCIで画面サイズが違うと、モバイルメニューが隠れたり、要素が重なったりする。`playwright.config.py` で `viewport` を厳格に指定しろ。
2. APIエンドポイントの切り替え: 環境変数 `BASE_URL` を使い、テスト環境のバックエンドを叩くように徹底すること。
3. タイムアウトの調整: CIはローカルより低スペックなことが多い。`expect` のデフォルトタイムアウトを少し長めに設定するだけでなく、「なぜ時間がかかっているのか」を特定するために、失敗時のスクリーンショットとトレースファイルを必ずアーティファクトとして保存すること。
最後に:完璧を目指すな、死守すべき場所を狙え
5年かけて学んだ結論は、「全部テストしようとすると、テストのメンテナンスで自分が死ぬ」ということだ。
- ログイン、決済、データ保存。
- これら「金と信頼」に関わるクリティカルなフローだけをE2Eで守る。
- 残りはユニットテストや統合テストに任せる。