深夜のデプロイで心臓を止めるな:Playwrightによる「落ちない」テスト自動化の極意
午前2時、本番環境のダッシュボードに浮かぶ「500 Internal Server Error」の文字。
原因は、ユーザープロフィール画面の微細なCSS修正に伴う、ReactコンポーネントのProps受け渡しミスだった。ログイン後のヘッダーにユーザー名が表示されず、APIが401を吐き出し続けている。
「テストを書けばよかった」。そんな後悔は、デプロイボタンを押した瞬間にしか意味をなさない。俺たちは、自分の書いたコードが動いていることを、感覚ではなく「証明」しなければならない。
この記事では、Seleniumの悪夢から抜け出し、Playwrightで泥臭く、しかし確実に「守りの要」を構築する方法を共有する。
1. なぜCypressではなくPlaywrightなのか?
「Cypressで十分じゃないか?」という声はもっともだ。だが、大規模なSPAを運用し、かつCI環境での実行コストを突き詰めると、Playwrightの優位性は圧倒的になる。
- ブラウザコンテキストの並列実行: Cypressは同一ブラウザプロセス内でテストを隔離するが、Playwrightはブラウザコンテキスト単位で完全に分離する。これが実行速度の「異常な速さ」の正体だ。
- ネットワークインターセプションの柔軟性: `page.route()` を使えば、特定のAPIレスポンスを動的にモック化し、わざとエラーを返してフロントエンドの例外処理をテストできる。これが実務においてどれほど強力か、一度体験すれば戻れない。
- マルチタブ・マルチブラウザ: 複数のタブを同時に操作し、チャットアプリのリアルタイム同期をテストするような芸当も、Playwrightなら標準機能だ。
2. 認証地獄からの脱却:`storageState`とCIの罠
ログイン処理を毎回テストの頭で行うのは時間の浪費だ。`storageState`で認証情報をシリアライズし、使い回すのが基本だが、ここに「CIの罠」がある。
CIで「パスが合わない」問題を撲滅する
CI環境とローカルでパスがズレてテストが落ちるのは、初心者あるあるだ。環境変数を使ってパスを動的に制御する構成にすべきだ。// playwright.config.js
import { defineConfig } from '@playwright/test';
import path from 'path';
const authFile = process.env.AUTH_STATE_PATH || path.join(__dirname, 'playwright/.auth/user.json');
export default defineConfig({
use: {
storageState: authFile,
},
});
GitHub Actionsでは、`actions/cache` を使ってログイン状態をキャッシュし、テスト実行前にログインスクリプトを走らせるフローが最強だ。これで「ローカルでは通るのに」という言い訳を物理的に消せる。
3. POMを超えた「Component Object Model」のすすめ
画面の要素セレクタを直書きするのは、未来の自分へのテロ行為だ。だが、ただのPage Object Model(POM)では不十分だ。UIコンポーネント単位で責務を切り出す、Component Object Modelのアプローチを推奨する。
アクセシビリティロール(`getByRole`)を軸にすることで、DOM構造が変わってもテストが壊れにくい「堅牢なセレクタ」が書ける。
// components/Navigation.js
export class Navigation {
constructor(page) {
this.page = page;
this.profileLink = page.getByRole('link', { name: /profile/i });
}
async navigateToProfile() {
await this.profileLink.click();
}
}
// テストコード
test('ユーザーはプロフィールに遷移できる', async ({ page }) => {
const nav = new Navigation(page);
await nav.navigateToProfile();
await expect(page).toHaveURL(/.*profile/);
});
`getByRole`を使うことで、テストが「ユーザーからどう見えるか」を記述するようになり、コードの保守性が劇的に向上する。
4. 大規模テストを爆速化する「Sharding」戦略
テストが100を超えた時、直列実行は「死」を意味する。GitHub Actionsの `sharding` を使えば、テストを分割して並列実行できる。
# .github/workflows/playwright.yml
jobs:
test:
strategy:
matrix:
shard: [1/3, 2/3, 3/3]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}
`1/3, 2/3, 3/3` と分割することで、実行時間は単純計算で1/3になる。大規模なE2Eスイートを抱える現場では、この設定なしではCIの待ち時間だけで寿命が削れる。
5. 最後に:テストは「書く」のではなく「育てる」ものだ
Playwrightを導入すれば、深夜のデプロイで震えることはなくなる。しかし、テストコードもまた「プロダクトの一部」だ。
- トレースファイルの活用: エラー発生時の動画とネットワークログをCIのアーティファクトとして保存しろ。原因特定までの時間が数時間から数秒に短縮される。
- 失敗を前提に設計する: 「なぜ落ちたか」が即座にわかるエラーメッセージを、アサーションの `message` 引数で丁寧に書く。
明日もまた、冷めたコーヒーを飲みながらデプロイするのか?それとも、緑色のテスト結果を見て安心して眠りにつくのか?決めるのは、今この瞬間のコードだ。