E2Eテストで消耗するな。Playwrightと「テストピラミッド」で構築する、死なない自動化パイプライン

E2Eテストで消耗するな。Playwrightと「テストピラミッド」で構築する、死なない自動化パイプライン

「またE2EでCIが落ちてるぞ」。

Slackに流れる真っ赤な通知を見て、絶望した経験はないか? 修正したはずのない箇所でテストが弾かれ、原因調査に1時間。結局「ただのタイミングの問題(Flaky Test)」でした、なんてオチ。そんな泥沼で消耗しているなら、今すぐテスト戦略を根本から見直すべきだ。

E2Eテストは「書くこと」よりも「いかに捨てて、いかにメンテコストを下げるか」という引き算の哲学がすべてだ。今回は、現場で泥をすすりながら辿り着いた、Playwrightを用いた堅牢なCI/CD構築の全貌を晒す。


1. テストピラミッドを再定義する:E2Eは「最後の砦」に過ぎない

多くのエンジニアが陥る罠は、「全部E2Eでテストしようとする」ことだ。UIの細かい色味や、コンポーネント単体の挙動までブラウザを立ち上げてテストするのは、コスト対効果が悪すぎる。

テスト層役割実行速度メンテコスト
E2Eテストユーザーのクリティカルパス(決済・ログイン等)遅い高い
コンポーネントテストUIパーツの単体挙動(Vitest + Testing Library)速い低い
ユニットテストビジネスロジックの検証超速い低い

E2Eテストは、「ユーザーが金銭を払う・データを保存する」といったビジネスの根幹(クリティカルパス)のみに絞る。それ以外はコンポーネントテストに逃がす。この境界線を引く勇気がないと、テストコードはただの「負債」に成り下がる。


2. 認証処理の「再利用」で、実行時間を劇的に削る

全テストケースでログイン画面を操作するのは、プログラミングにおける罪だ。認証基盤が変わるたびに数百のテストが爆死するなんて御免だろう。

Playwrightの `storageState` を使い、ログイン後のCookieを保存して使い回すのが定石だ。ただし、注意点が一つ。トークンの有効期限切れだ。CI環境では定期的に認証状態を更新する仕組み(`global.setup.ts`)を分離し、常に新鮮なセッションを注入しろ。

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'setup',
      testMatch: /global.setup\.ts/,
    },
    {
      name: 'chromium',
      use: {
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});


3. 「動的なDOM」に振り回されないための鉄則

`page.click('.btn-submit')` と書いて放置している奴は、今すぐコードを消せ。Reactのような動的フレームワークでは、要素が存在してもイベントリスナーがアタッチされる前だったり、アニメーション中でクリックが弾かれたりする。

常に「ユーザーが見ている役割(Role)」で取得し、状態を明示的に待て。

test('ユーザー設定を保存できること', async ({ page }) => {
  await page.goto("/settings");
  
  // getByRoleは、アクセシビリティツリーを辿るため堅牢性が高い
  const submitButton = page.getByRole("button", { name: /保存する/i });
  
  // 状態の確定を待ってから操作する
  await expect(submitButton).toBeEnabled();
  await submitButton.click();
  
  // 完了メッセージの出現を待つ(明示的なアサーション)
  await expect(page.getByText("保存しました")).toBeVisible({ timeout: 5000 });
});


4. GitHub Actions:DBの死を招かないために

CI環境でテストを回す際、最も多い失敗は「DBの汚染」だ。テストを並列実行してデータが衝突したり、前回のゴミデータでテストが失敗したりする。

Docker Composeを使う場合、単にコンテナを立てるだけでは不十分だ。「DBが完全にReadyになるまで待機する(Healthcheck)」という手順をサボるな。これをしないと、DB起動前にテストが走り出し、不可解なエラーに悩まされることになる。

# .github/workflows/test.yml
services:
  db:
    image: postgres:15
    env:
      POSTGRES_PASSWORD: password
    # 接続可能になるまで待機させる設定が必須
    options: >-
      --health-cmd "pg_isready"
      --health-interval 5s
      --health-timeout 5s
      --health-retries 5


5. 結論:自動テストは「保険」に過ぎない

自動テストは魔法じゃない。ただの「安心を買うための保険」だ。

もしテストコードのメンテに週の半分を費やしているなら、そのテストは捨てろ。テストがプロダクトの進化を阻害するなら、それは本末転倒だ。

1. クリティカルパスに絞る:それ以外はユニット/コンポーネントテストで担保する。
2. セレクタは堅牢に:`getByRole` を使い、実装詳細ではなくユーザー体験をテストする。
3. CIは環境をクリーンに:DBのライフサイクルをテストパイプラインに組み込む。

完璧主義はエンジニアを殺す。泥臭く、しかし論理的に。動くプロダクトを作り続けるために、テストという「保険」を賢く運用してくれ。