E2Eテストで「画面を網羅」しようとする奴は、半年後に地獄を見る

E2Eテストで「画面を網羅」しようとする奴は、半年後に地獄を見る

E2Eテストを書いていて「全画面の全要素をテストしたい」なんて息巻いている奴は、間違いなく半年後に地獄を見る。Seleniumで消耗し、深夜3時に「なぜか落ちるテスト」のログを眺めていたあの頃の俺に教えてやりたい。「お前が追うべきなのはUIの網羅性じゃない、ユーザーの体験(UX)そのものだ」と。

モダンなWeb開発において、UIコンポーネントの挙動を個別に叩くのは単体テストやStorybookの仕事だ。E2Eの役割はただ一つ。「ユーザーがログインし、商品を選び、決済を終える」というクリティカルパスが一本の線として繋がっているかを確認すること。これさえ守れば、テストコードは驚くほどシンプルになり、メンテナンスコストは劇的に下がる。


1. ユーザーの「体験」をコードに落とし込む

Playwrightがなぜ最強なのか。それは、Playwrightが単なる「ブラウザ操作ツール」ではなく、「ユーザーの振る舞い」を直感的に記述できるからだ。

// ログインから決済までのクリティカルパス(TypeScript)
test('ユーザーが正常に購入を完了できる', async ({ page }) => {
  await page.goto('/login');
  
  // ユーザーの操作をそのままコードにする
  await page.getByLabel('メールアドレス').fill('[email protected]');
  await page.getByLabel('パスワード').fill('password123');
  await page.getByRole('button', { name: 'ログイン' }).click();

  // URLの遷移も自動待機(Auto-waiting)が効く
  await expect(page).toHaveURL('/dashboard');
  
  await page.getByRole('button', { name: '購入する' }).click();
  await expect(page.getByText('決済が完了しました')).toBeVisible();
});

ここで重要なのは、`page.click()` などのメソッドが、対象要素が「操作可能になるまで」を自動的に待機してくれる点だ。無理な `sleep` を入れるのは素人のやること。まずはPlaywrightの強力なAuto-waitingを信頼しろ。


2. 非同期通信という「罠」をどう捌くか

SPA全盛の今、テストが気まぐれに落ちる(Flaky Test)最大の原因は「DOMは存在するが、APIのレスポンスがまだ戻っていない」というラグだ。

基本は `expect(locator).toBeVisible()` で十分だが、複雑な決済フローなどで「APIの完了を確実にフックしたい」という場面は必ず来る。そんなときは `expect_response` を使え。

test('APIレスポンスを待機して検証する', async ({ page }) => {
  // 最後の手段としてネットワーク待機を併用する
  const responsePromise = page.waitForResponse('**/api/v1/order');
  
  await page.getByRole('button', { name: '確定' }).click();
  
  const response = await responsePromise;
  expect(response.status()).toBe(200);
});


3. なぜ「CSSセレクタ」を捨てて `data-testid` を使うのか

昔の俺は `div > div > span:nth-child(2)` のような地獄のセレクタを書いていた。UIの修正一つでテストが全滅し、週末を潰した経験は一度や二度じゃない。

解決策はシンプルだ。「CSS(見た目)」と「テスト(責務)」を完全に分離すること。

CSSクラスはデザイン変更で明日消えるかもしれないが、`data-testid` はテストのために存在する。これを使うだけで、メンテナンスコストは1/10になる。UIがどれだけリニューアルされようが、IDさえ残っていればテストは落ちない。これが現場の知恵だ。


4. CI/CDに組み込まないテストは「ただの負債」

ローカルで動くテストなんて自己満足でしかない。GitHub Actionsでプルリクのたびに走らせ、失敗したらマージをブロックする。この強制力こそが開発の正義だ。

ただし、キャッシュ戦略を無視したCIは「遅すぎて使わなくなる」。実行時間が5分を超えた瞬間、エンジニアはテストを無視し始める。

# .github/workflows/playwright.yml
name: Playwright Tests
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - name: Cache node_modules
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test


結論:何を知っていれば勝てるのか

1. クリティカルパス以外はテストしない: UIの細かい装飾は単体テストの領分。E2Eでは「金が動く場所」や「ログイン」といった主要動線だけを死守しろ。
2. `data-testid` を徹底する: CSSセレクタでDOM構造に依存するのはやめろ。責務を分離せよ。
3. CIで強制実行し、キャッシュを効かせる: 実行しないテストはただのゴミだ。速く、確実に、自動で。

E2Eテストは「完璧なテストスイート」を目指すものじゃない。「壊れたら即座に気づける仕組み」を作ること。この視点を持つだけで、お前の開発ライフは劇的に楽になるはずだ。

さあ、Seleniumの亡霊は成仏させて、明日からPlaywrightで「ユーザーの体験」を守りに行こうぜ。