「テストは通っているはずなのに」——Playwright導入でハマった、不安定なE2Eテストとの格闘記
「ユニットテストは全部グリーンだし、ロジックも検証済み。なのに、なぜかデプロイ直後にユーザーから『ログインできない』という報告が届く」。
数ヶ月前、私はこの絶望的な状況に直面した。Vitestで関数の挙動を固め、TypeScriptで型安全を担保し、リグレッションを防いでいるつもりだった。しかし、UIの要素がレンダリングされるタイミングや、APIレスポンス待ちの隙間に発生する「不整合」までは、ユニットテストでは防げなかったのだ。
結局、原因はフロントエンドの複雑な状態遷移(State)と、非同期処理のわずかなズレにあった。これに気づいたとき、私は「単体テストだけでは足りない、E2Eテストが必要だ」と痛感した。そうして、Playwrightの導入を決めたわけだが、そこからは「テストが通らない」のではなく「テストが時々失敗する(Flakyな状態)」という、もっと厄介な問題との戦いが始まった。
最初は、とにかくテストを成功させることだけを考えていた。例えば、要素が表示されるまで `page.waitForTimeout(3000)` のように、強引にスリープを入れるコードを書きまくった。ローカル環境の爆速なマシンではこれで問題なく動いていたのだ。しかし、GitHub ActionsでCIを回し始めた途端、地獄が始まった。
CI環境は、ローカルのマシンよりもリソースが限られている。ネットワークの遅延やCPUの負荷によって、3秒待機しても要素が現れないことが頻発した。「さっきは通ったのに、次は失敗する」という、いわゆるFlaky Test(不安定なテスト)だ。これではテストの意味がない。CIのビルドが落ちるたびに、原因がコードのバグなのか、単なるタイミングの問題なのかを調査する作業に時間を奪われることになった。
この問題を解決するために学んだのは、Playwrightが持つ「Web-first Assertions」を正しく使うことだった。Playwrightには、要素が特定の状態(VisibleやEnabledなど)になるまで自動的にリトライしてくれる仕組みがある。これを使えば、不適切な `sleep` を排除できる。
// 失敗しやすい書き方:強引な待機に依存している
test('bad-example: sleepを使った不安定なテスト', async ({ page }) => {
await page.goto('/dashboard');
await pageta.click('#submit-button');
// 要素が表示されるまで、ただ時間を待つ。これではCI環境で失敗する。
await page.waitForTimeout(3000);
const successMessage = await page.textContent('.success-msg');
expect(successMessage).toBe('保存完了');
});
// 推奨される書き方:Web-first Assertionsを利用する
test('good-example: auto-retrying assertions', async ({ page }) => {
await page.goto('/dashboard');
await page.click('#submit-button');
// expect(locator).toBeVisible() は、要素が条件を満たすまで自動でリトライしてくれる。
// ネットワークの遅延やレンダリングのラグがあっても、タイムアウト内なら成功する。
const successMessage = page.locator('.success-msg');
await expect(successMessage).toBeVisible();
await expect(successMessage).toHaveText('保存完了');
});
この「自動リトライ」を意識するだけで、テストの安定性は劇的に向上した。また、CI環境(GitHub Actions)への組み込みにおいても、ブラウザの依存関係で躓いたが、「`npx playwright install --with-deps`」を実行するようにワークフローを修正することで、環境構築の苦労も解消できた。
E2Eテストは、ユニットテストのように「ロジックの正しさ」を検証するものではない。「ユーザーの体験(User Flow)が壊れていないか」を保証するためのものだ。そのためには、要素がどう見えるか、どう操作可能かといった、「状態の遷移」に焦点を当てたアサーションを書く必要がある。
導入当初は、テストコードを書くこと自体が「コスト」のように感じていた。しかし、今ではCIで回るPlaywrightの緑色のチェックマークを見るたびに、デプロイに対する心理的なハードルが下がったことを実感している。失敗を繰り返してようやく辿り着いた、安定した自動化への道のりだった。