.github/workflows/playwright.yml

「E2Eテストが落ちたから再実行ポチー」
この1クリックが、お前の給料をドブに捨てている自覚はあるか?

CIが赤く染まるたびに溜息をつき、再実行ボタンを押してコーヒーを淹れに行く。そんな「祈るだけのルーチン」を繰り返しているなら、今すぐエンジニアを辞めたほうがいい。それはテストじゃない。ただの儀式だ。

俺も昔はそうだった。GitHub Actionsのログを眺めながら、「今日はネットワークの調子が悪いのか?」と自分を慰めていた。だが、現実は残酷だ。不安定なテストを放置するのは、時限爆弾のタイマーを自分から手動で進めているのと同じことだ。

今日は、俺が「Flaky Test(不安定なテスト)」という名の悪霊に夜中まで振り回され、ようやく辿り着いた「戦えるE2Eテスト」の全貌を晒す。


1. `waitForTimeout`は「敗北」の証だ

初心者が一番最初にやる間違いがこれだ。「とりあえず3秒待てば読み込むだろ」という、思考停止の極み。これを入れた瞬間に、お前のコードは保守不可能なゴミと化す。

ネットワークが速い時は無駄な待ち時間が発生し、遅い時は結局落ちる。百害あって一利なしだ。Playwrightには強力なオートウェイト機能がある。明示的な待機なんて、本当にどうしようもない時以外はコードから抹殺しろ。

// 悪い例:思考停止の3秒待機。お前のキャリアを3秒ずつ削り取っている
await page.click('#submit-button');
await page.waitForTimeout(3000); 
await expect(page.locator('.success-msg')).toBeVisible();

// 良い例:状態をトリガーに待つ。これぞエンジニアリング
await page.click('#submit-button');
// 成功メッセージが出るまで待つ。5秒待っても出ないなら、それはお前のテストが正しい(バグを検知したのだ)
await expect(page.locator('.success-msg')).toBeVisible({ timeout: 5000 });

俺がこれを全廃した時、テスト全体の実行時間が15分から3分に縮んだ。あの時の虚無感は、今でも忘れられない。


2. 並列実行を「ケチる奴」は生き残れない

テストが遅い?なら並列で回せ。GitHub Actionsの`shard`を使わない手はない。100個のテストを4つに分割して、物理的に時間を4分の1にする。

# .github/workflows/playwright.yml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
    steps:
      - uses: actions/checkout@v4
      - name: Run Playwright tests
        run: npx playwright test --shard=${{ matrix.shard }}

ここで「テスト間の依存関係」を残している奴は、並列化した瞬間に地獄を見る。E2Eは「独立していること」が正義だ。DBのクリーンアップは各テストの`beforeEach`で完結させろ。依存関係があるテストは、テストではなく「連鎖爆発装置」だ。


3. トレースビューアという名の「黒箱の解読器」

CI上で落ちた時、ログだけ見て原因がわかる天才なんていない。Playwrightのトレースビューアは神だ。これを使わない奴は、目隠しして高速道路を歩いているようなもんだ。

// playwright.config.ts
export default defineConfig({
  use: {
    // 失敗した時だけトレースを残せ。成功した時のログなんてゴミだ
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
});

かつて俺は、APIのレスポンスが0.1秒遅れるだけで落ちるテストに数日を溶かした。トレースビューアで動画をコマ送りして、フロントのローディング判定が物理的に不可能だったと気づいた時の脱力感といったらなかったね。ツールを使え。勘に頼るな。


4. Flaky Testは「排除」ではなく「飼い慣らせ」

「Flaky Testは悪だ、すべて撲滅せよ」なんて綺麗事は、外部APIが絡む現代のWeb開発では通用しない。外部サービスが死ぬ時は死ぬ。だからこそ、Retriesを戦略的に使う。

// playwright.config.ts
export default defineConfig({
  // ローカルでは0、CI環境なら2回まで許容。これが現実的な防衛線だ
  retries: process.env.CI ? 2 : 0,
});

ただし、Retriesに頼りすぎると「テストが落ちていることに気づかない」という最悪の事態になる。失敗したら即座にSlackへ叩き込め。

# GitHub Actionsの失敗を通知する泥臭い一手
curl -X POST -H 'Content-type: application/json' --data '{"text":"🚨 E2Eテストが死んだぞ。トレースを確認しろ: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' $SLACK_WEBHOOK_URL


5. E2Eは「コスト」だ。削ぎ落とせ

最後に一つ、耳の痛い話をしよう。E2Eテストは万能じゃない。カバレッジ100%なんて自己満足の極みだ。

俺はかつて、全画面の全ボタンをテストしようとして、テストコードの保守だけで開発工数の40%を消費していた。ある日、思い切って重要度の低いテストを30%削除した。結果、開発速度は爆上がりし、バグの発見率も変わらなかった。

「金が動く箇所」だけを堅牢にしろ。
それ以外はユニットテストでカバーすればいい。テストコードの保守に追われて機能開発が止まるなど、本末転倒もいいところだ。

テストは「楽をするため」にあるんじゃない。「安心して寝るため」にある。
E2Eは「書く」ことより「捨てる」ことの方が100倍難しい。お前も、その過剰なテストコードを一度見直してみろ。案外、いらないものだらけだぞ。