E2Eテストで死なないために。Playwrightで「負債」を「資産」に変える設計術

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テストを書く目的は、開発速度を落とすことではなく、「安心してコードを破壊するため」にある。

テストが落ちて修正に追われる日々を過ごしているなら、一度立ち止まって考えてみてほしい。そのテストは、本当にユーザーの価値を守っているか? それとも、過去の仕様に縛り付けられているだけではないか?

テストコードは、君の最強の味方にもなれば、最大の足かせにもなる。賢く設計し、泥臭くメンテナンスし、そして何より、「テストが落ちたときに笑って直せる」くらいの余裕を持ったテストスイートを構築してほしい。

さあ、次はどのテストを削除して、どのテストを書き直す?