Playwrightで「CIでだけ落ちるテスト」を撲滅する—泥臭い現場の最適化戦略
「ローカルでは通るのに、なぜかCI環境でだけテストが落ちる」。
この悪夢のような通知に、何度深夜まで頭を抱えたことか。多くのエンジニアはこれを「CI環境の不安定さ」や「ネットワークの遅延」のせいにするが、現実はもっと残酷だ。その失敗の9割は、環境のせいではなく、テストコードの設計そのものが「CIという過酷な戦場」に耐えうる設計になっていないことに起因する。
E2Eテストは、単にブラウザを動かすスクリプトではない。プロダクトの品質を守る最後の砦だ。今回は、私が数々の失敗から学んだ、現場で即戦力となるPlaywrightのCI統合術を共有する。
1. ログイン処理の「使い回し」が最強の最適化
全テストでログインフォームを叩くのは、非効率の極みだ。テスト実行時間を浪費するだけでなく、ログイン機能そのものに障害が起きた瞬間、全テストが連鎖的に沈没する。
解決策は、認証後の`storageState`を再利用することだ。ただし、ここにはセキュリティとメンテナンスのバランスが必要になる。
// auth.setup.ts
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
// ログイン処理をここに集約
await page.goto('/login');
await page.fill('input[name="email"]', process.env.USER_EMAIL!);
await page.fill('input[name="password"]', process.env.USER_PASSWORD!);
await page.click('button[type="submit"]');
// 認証後のCookieとLocalStorageを保存
await page.context().storageState({ path: authFile });
});
重要な注意点:
この手法は認証トークンのライフサイクル管理を適切に行う必要がある。CI上で古いトークンを使ってテストが落ちるのを防ぐため、`storageState`の有効期限チェックや、CIパイプラインの冒頭でログインセッションを強制的に更新する仕組みを併用するのがプロの流儀だ。
2. 「waitForTimeout」はテストの敗北
`page.waitForTimeout(3000)` を書いた瞬間に、そのテストは「Flaky(不安定)」への道を歩み出す。ネットワーク速度やCPU負荷が変われば、3秒は一瞬で無意味になるからだ。
Playwrightの真骨頂は、強力なオートウェイト機能にある。
// 悪い例:魔法の数字を待つ
await page.waitForTimeout(2000);
await page.click('.submit-button');
// 良い例:状態を待つ
const submitBtn = page.locator('.submit-button');
await expect(submitBtn).toBeVisible();
await expect(submitBtn).toBeEnabled();
await submitBtn.click();
`toBeVisible()` や `toBeEnabled()` を使うことで、DOMの準備完了を「検知」して動く。これにより、テスト実行時間は極限まで短縮され、再現性の高いテストが実現する。
3. テスト容易性を考慮したフロントエンド設計
かつて「CSSのz-indexが重なっていてボタンが押せない」という理由で、`force: true` を使って無理やりテストをパスさせたことがある。しかし、これはユーザーの体験を無視したハックに過ぎない。
テストが落ちることは、UI設計における「警告」だ。
クリックできない要素があるなら、それはユーザーにとっても操作不能なUIかもしれない。テストのためにCSSを調整するのではなく、テストしやすいUIコンポーネント設計へとフィードバックを回せ。テストコードは、プロダクトの健全性を映し出す鏡なのだ。
4. CIパイプラインのデバッグ効率を最大化する
CI環境でテストが落ちた際、画面を見ずに原因を特定するのは至難の業だ。以下の設定を `playwright.config.ts` に加えることで、デバッグ効率は劇的に向上する。
// playwright.config.ts
export default defineConfig({
reporter: [['list'], ['blob']], // Blobレポーターで結果を保存
use: {
trace: 'retain-on-failure', // 失敗時のみトレースを残す
screenshot: 'only-on-failure', // 失敗時のみスクリーンショット
},
});
GitHub Actionsのワークフローでは、`test-results` をアーティファクトとして保存するように設定せよ。
# .github/workflows/playwright.yml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: playwright-report/
retention-days: 7
また、テストを並列化する `sharding` は必須だ。時間を金で買う感覚で、コンテナを分割して実行時間を削り取れ。
結論:保守コストと速度のトレードオフを理解せよ
E2Eテストは、書く量よりも「壊れないテストをどう維持するか」にエンジニアリングの価値がある。
1. 認証を分離し、セッションを使い回せ。
2. `waitForTimeout`を追放し、DOMの状態を待て。
3. 失敗時のトレースを資産として残せ。
完璧なテストカバレッジを追い求めて、メンテナンス不能な巨大なテストコードの墓場を作るな。重要なのは、デプロイのたびに「自信を持ってボタンを押せる」という安心感だ。
テストは、プロダクトを止める足枷ではない。お前のチームが爆速でデリバリーを繰り返すための、最強のガードレールに他ならない。さあ、今すぐコードを見直して、そのFlakyなテストを「動く資産」に変えてやれ。