E2Eテストで死なないために。Playwrightで「負債」を「資産」に変える設計術
「テストは完璧だ」。そう確信してデプロイした金曜の夜、画面が真っ白に染まった瞬間、俺の心臓は止まった。原因は、フロントエンドが新しいAPIのレスポンス形式に切り替わった際、古いキャッシュがブラウザに残ったままレンダリングが走り、JSが死んでいたことだった。
テストコードはパスしていた。だが、それは「理想的な環境」で動いていただけで、現実のブラウザの挙動を全く模倣できていなかった。俺が書いていたのは、プロダクトを守る盾ではなく、ただの「自己満足の儀式」だったんだ。
1. なぜ「CSSセレクタ直書き」は地獄への直行便なのか
初心者が最初に陥る罠がこれだ。`page.locator('.btn-primary-01')`。一見便利だが、これはUIの変更と共にテストが雪崩のように崩壊する「時限爆弾」だ。
Playwrightが推奨するUser-facing locators(`get_by_role`など)を使うべき理由は、単にコードが綺麗になるからじゃない。「ユーザーがどう操作するか」という視点と、アクセシビリティの整合性が取れるからだ。
# 悪い例:実装依存。CSSクラス名が変われば即死する
page.locator(".css-12345").click()
# 良い例:ユーザーの体験に近い。アクセシビリティツリーを辿るため堅牢
page.get_by_role("button", name="ログイン").click()
`get_by_role`を使えば、ボタンの見た目が変わろうが、スタイルが崩れようが、「ログイン」という役割を持つ要素がある限りテストは通る。これこそが、変更に強いテストを書くための第一歩だ。
2. POM(Page Object Model)を「ただのクラス」にするな
POMは単なるコードの整理術ではない。「ページ状態の抽象化」だ。単にlocatorを並べるだけなら誰でもできる。重要なのは、ページ遷移や非同期待ちを隠蔽することにある。
from playwright.sync_api import Page, expect
class LoginPage:
def __init__(self, page: Page):
self.page = page
# Locatorは遅延評価される。ここで定義しても通信は発生しない
self.username = page.get_by_label("ユーザー名")
self.password = page.get_by_label("パスワード")
self.submit = page.get_by_role("button", name="ログイン")
def login(self, user: str, pass_: str):
self.username.fill(user)
self.password.fill(pass_)
self.submit.click()
# 遷移後の状態をここで定義しておくことで、呼び出し側をクリーンに保つ
self.page.wait_for_url("**/dashboard")
この設計の肝は、テストスクリプト側で「遷移待ち」を意識させないことだ。`LoginPage`が「ログインしたらダッシュボードへ行く」という知識をカプセル化することで、テストコードは劇的に読みやすくなる。
3. Flaky Testは「環境のせい」ではなく「待ち方のせい」
「たまに落ちる」は、エンジニアにとって最大の恥だ。`page.wait_for_timeout(3000)` を書いた時点で負け確定だ。
Playwrightの `expect` は、内部でWeb-first assertionsという仕組みを持っていて、対象の要素が出現するまで自動的にリトライを繰り返す。`--retries` に頼るのは、この強力な仕組みを理解していない素人のやることだ。
# 良くない:無理やり待つ
page.wait_for_timeout(5000)
expect(page.locator(".toast")).to_have_text("成功")
# 良い:Web-first assertionsに任せる
# 自動的にリトライし、タイムアウトまで待機してくれる
expect(page.locator(".toast")).to_have_text("成功", timeout=10000)
テストが落ちる原因の9割は「要素が見つかる前に操作しようとしている」ことにある。DOMの更新を待つのではなく、期待する状態になるまで待つ。この思考の転換が、不安定なテストを撲滅する。
4. テスト自動化「優先度マトリクス」
個人開発で全てを自動化するのはコスト対効果が悪すぎる。俺は以下の基準で「捨てるテスト」と「守るテスト」を決めている。
| 優先度 | 対象機能 | 理由 |
|---|---|---|
| 高 | ログイン・決済・データ保存 | 壊れるとビジネスが死ぬコア機能 |
| 中 | フォーム入力・主要な遷移 | ユーザー体験の根幹 |
| 低 | 細かなUIアニメーション | メンテナンスコストが壊滅的 |
俺の運用ルール:
1. Visual Regression Testingは導入しない:少しのフォントレンダリングの差で落ちるテストは、ただのストレス源だ。
2. CI時間は5分以内:これを超えるなら、テストを並列化するか、そもそもテストを削る。
3. カバレッジは追わない:重要なフローを網羅できているなら、カバレッジが60%でも100%でも変わらない。
結論:テストは「自分を縛る鎖」ではない
E2Eテストを書く目的は、開発速度を落とすことではなく、「安心してコードを破壊するため」にある。
テストが落ちて修正に追われる日々を過ごしているなら、一度立ち止まって考えてみてほしい。そのテストは、本当にユーザーの価値を守っているか? それとも、過去の仕様に縛り付けられているだけではないか?
テストコードは、君の最強の味方にもなれば、最大の足かせにもなる。賢く設計し、泥臭くメンテナンスし、そして何より、「テストが落ちたときに笑って直せる」くらいの余裕を持ったテストスイートを構築してほしい。
さあ、次はどのテストを削除して、どのテストを書き直す?