E2Eテストが軽視されがちな理由と、その代償
ユニットテストはしっかり書いているチームでも、stagingにデプロイしてから「決済ボタンがモバイルで動かない」「SafariでログインFormがエラーになる」と気づくことがあります。自分もまさにこの経験をしました — Reactプロジェクトでユニットテストのカバレッジが82%あったにもかかわらず、CSSのz-indexがsubmitボタンを隠してしまい、iPhoneでチェックアウトフローが完全に壊れていました。
ユニットテストはそのエラーを捕捉できません。各関数を個別にテストするからです。E2Eテストは違います — 実際のユーザー操作を忠実にシミュレートします:ブラウザを開き、フォームに入力し、ボタンをクリックし、結果を確認します。
以前はSeleniumが主流でしたが、セットアップが煩雑で、WebDriverはバージョンがずれやすく、実行速度も遅い。CIで80本のSeleniumテストスイートが12分かかっていた経験があります。PlaywrightはMicrosoftが2020年にリリースし、これらの問題をほぼすべて解決しました:モダンなAPI、Chromium/Firefox/WebKitのサポート、デフォルトで並列実行 — 同等のスイートが約2〜3分で完了します。
この記事では、インストールから実際のWebアプリケーションでのテスト実行まで順を追って解説します — E2Eテストを初めて書く方にも最適です。
Playwrightのインストール
必要な環境
- Node.js 18以上(
node -vで確認) - npmまたはyarn
プロジェクトの初期化
最も手っ取り早い方法は、Playwright組み込みのinitコマンドを使うことです:
npm init playwright@latest
いくつか質問されます:
- TypeScriptかJavaScriptか? → プロジェクトがTSを使っているならTypeScriptを選択
- テストを格納するディレクトリ? → デフォルトは
tests/ - GitHub Actionsのワークフローを追加する? → CIを使っているならYes
- ブラウザをインストールする? → Yes
実行後のディレクトリ構造はこのようになります:
my-project/
├── tests/
│ └── example.spec.ts
├── playwright.config.ts
├── package.json
└── node_modules/
ブラウザを手動でインストールする場合
# すべてのブラウザをインストール
npx playwright install
# またはChromiumのみをインストール
npx playwright install chromium
詳細な設定
playwright.config.tsファイル
playwright.config.tsはテストスイート全体の動作を制御する場所です — タイムアウト、ブラウザ、リトライ、レポートなど。以下はベースラインとしてよく使う設定です:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// テストファイルのディレクトリ
testDir: './tests',
// 最大4テストを並列実行
workers: 4,
// テスト失敗時に1回リトライ(不安定なテストを減らす)
retries: 1,
// 各テストのタイムアウト:30秒
timeout: 30_000,
// レポート:HTMLレポート+ターミナル
reporter: [['html', { open: 'never' }], ['list']],
use: {
// ベースURL — ハードコードの代わりにbaseURLを使用
baseURL: 'http://localhost:3000',
// テスト失敗時にスクリーンショットを撮影
screenshot: 'only-on-failure',
// テスト失敗時に動画を記録
video: 'retain-on-failure',
// デバッグ用トレースを有効化(各ステップを後から確認できる)
trace: 'on-first-retry',
},
// 複数ブラウザでテスト
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
],
// テスト前に開発サーバーを自動起動
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
最初のテストを書く
実際のテストを書いてみましょう。tests/login.spec.tsを作成します:
import { test, expect } from '@playwright/test';
test.describe('ログインページ', () => {
test('有効な情報でログイン成功', async ({ page }) => {
// ログインページに移動
await page.goto('/login');
// フォームに入力
await page.fill('[name="email"]', '[email protected]');
await page.fill('[name="password"]', 'secret123');
// submitボタンをクリック
await page.click('button[type="submit"]');
// ダッシュボードへのリダイレクトを確認
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('ようこそ');
});
test('パスワードが間違っている場合にエラーを表示', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', '[email protected]');
await page.fill('[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
// エラーメッセージの表示を確認
await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toContainText('パスワードが間違っています');
});
});
Page Object Modelでコードを再利用する
テストを増やすにつれて、セレクターをあちこちコピーペーストしていることに気づきます。Page Object Model(POM)は、あるページのすべてのセレクターを一つのクラスにまとめることでこの問題を解決します:
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('[name="email"]');
this.passwordInput = page.locator('[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
テストで使用する:
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('ログイン成功', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'secret123');
await expect(page).toHaveURL('/dashboard');
});
テストの実行と結果の確認
基本的なテスト実行コマンド
# すべてのテストを実行
npx playwright test
# 特定のファイルを実行
npx playwright test tests/login.spec.ts
# Chromeのみで実行
npx playwright test --project=chromium
# ブラウザを表示するヘッドモードで実行
npx playwright test --headed
# テスト名で指定して実行
npx playwright test -g "有効な情報でログイン成功"
# HTMLレポートを開く
npx playwright show-report
Trace Viewerでデバッグする
テストが失敗してどこからデバッグすればいいかわからない時 — そんな時こそTrace Viewerの出番です。動画のように各ステップを記録し、各時点でのDOMスナップショット、ネットワークリクエスト、コンソールログも一緒に残してくれます — console.logを一行も追加する必要がありません。
# トレースを有効にして実行
npx playwright test --trace on
# 最後の実行のトレースを開く
npx playwright show-trace test-results/login-chromium/trace.zip
自分のデバッグフロー:テスト失敗 → トレースを開く → エラーが起きたステップのスクリーンショットを確認 → DOMでセレクターが正しいかチェック。散在するログでテストを何度も再実行するより、はるかに速いです。
GitHub ActionsでCI/CDに組み込む
initで「Yes」を選んだ場合、Playwrightがすでにワークフローファイルを生成しています。GitHub Actions CI/CDの基本を押さえておくと、よりスムーズにパイプラインを構成できます。よく使うテンプレートはこちらです:
# .github/workflows/playwright.yml
name: Playwright E2E Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
CI: true
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
実践から学んだTips
セレクターの優先順位は:data-testid → ARIAロール → ラベル → テキスト → CSSクラス。CSSクラスとXPathはUI refactor時に壊れやすい — クラス名を一つ変えるだけで何十ものテストが一瞬で赤くなります。Pre-commitフックとESLintでコード規約を強制すると、セレクターの書き方もチーム内で統一しやすくなります。
// 良い — 安定しており、スタイリングの影響を受けない
await page.click('[data-testid="submit-btn"]');
await page.click('role=button[name="ログイン"]');
// 避ける — 壊れやすい
await page.click('.btn.btn-primary.login-form__submit');
ネットワークモックも、他の多くのツールよりPlaywrightが優れている点の一つです。実データを削除せずに「商品がありません」画面をテストしたい場合は、page.route()でインターセプトできます:
// 空のリストを返すAPIのモック
await page.route('**/api/products', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
await page.goto('/products');
await expect(page.locator('.empty-state')).toBeVisible();
テストデータの準備によく使うツールもあります:toolcraft.app — APIからのJSONレスポンスを素早くフォーマットして、アサーションを書く前に構造を確認できます。ブラウザ拡張機能をインストールするより手軽です。
まとめ
Playwrightは小規模から大規模なプロジェクトまで対応できます — シンプルなAPI、充実したデバッグ機能、高速な実行。まず最も重要なフロー(ログイン・認証、決済、フォーム送信)のテストから始めて、徐々に範囲を広げていくのがおすすめです。
Seleniumとの比較:WebDriverが不要、セレクターAPIがはるかに賢く、デバッグ時のTrace Viewerは他の追随を許しません。Cypressを使っている方にもPlaywrightを試す価値があります — より多くのブラウザをサポートし、追加設定なしでデフォルトで並列実行できます。

