Playwrightは、Microsoft製の次世代ブラウザテスト自動化ツールです。Chromium、Firefox、WebKitをサポートし、高速で信頼性の高いE2E(End-to-End)テストを実現します。本記事では、Playwrightの基礎から実践的なテストパターンまで、実際のコードを通じて学びます。
この記事で学ぶこと
- Playwrightの環境セットアップ
- 基本的なテストの書き方
- Page Object Modelの実装
- 認証フローのテスト
- APIモックとインターセプト
- CI/CDへの統合
環境構築
インストール
# 新規プロジェクトの場合
npm init playwright@latest
# 既存プロジェクトへの追加
npm install -D @playwright/test
npx playwright install
初期化時に以下の選択肢が表示されます。
? Do you want to use TypeScript or JavaScript? › TypeScript
? Where to put your end-to-end tests? › tests
? Add a GitHub Actions workflow? › true
? Install Playwright browsers? › true
プロジェクト構成
project/
├── playwright.config.ts # 設定ファイル
├── tests/
│ ├── example.spec.ts # テストファイル
│ └── fixtures/ # カスタムフィクスチャ
│ └── auth.ts
├── pages/ # Page Objects
│ ├── login.page.ts
│ └── dashboard.page.ts
├── test-results/ # テスト結果(自動生成)
└── playwright-report/ # HTMLレポート(自動生成)
設定ファイル
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// テストディレクトリ
testDir: './tests',
// 並列実行
fullyParallel: true,
// CI環境ではリトライを設定
retries: process.env.CI ? 2 : 0,
// ワーカー数
workers: process.env.CI ? 1 : undefined,
// レポーター設定
reporter: [
['html', { open: 'never' }],
['list'],
],
// 共通設定
use: {
// ベースURL
baseURL: 'http://localhost:3000',
// スクリーンショット設定
screenshot: 'only-on-failure',
// トレース設定
trace: 'on-first-retry',
// ビデオ録画
video: 'on-first-retry',
},
// ブラウザ設定
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// モバイルデバイス
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 13'] },
},
],
// 開発サーバーの起動
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
基本的なテストの書き方
最初のテスト
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test.describe('ホームページ', () => {
test('タイトルが正しく表示される', async ({ page }) => {
await page.goto('/');
// タイトルの検証
await expect(page).toHaveTitle(/My App/);
});
test('ナビゲーションが機能する', async ({ page }) => {
await page.goto('/');
// リンクをクリック
await page.getByRole('link', { name: 'About' }).click();
// URLの検証
await expect(page).toHaveURL('/about');
// コンテンツの検証
await expect(page.getByRole('heading', { level: 1 })).toHaveText('About Us');
});
});
ロケーター戦略
Playwrightでは、堅牢なロケーター(要素の特定方法)を使用することが重要です。
// 推奨: ユーザー視点のロケーター
page.getByRole('button', { name: '送信' }); // ARIA role
page.getByLabel('メールアドレス'); // ラベル
page.getByPlaceholder('example@email.com'); // プレースホルダー
page.getByText('ログインに成功しました'); // テキスト
page.getByTestId('submit-button'); // data-testid
// 非推奨: 実装詳細に依存するロケーター
page.locator('#submit-btn'); // ID(変更されやすい)
page.locator('.btn-primary'); // クラス(スタイル変更で壊れる)
page.locator('div > button:nth-child(2)'); // 構造依存
アサーション
import { test, expect } from '@playwright/test';
test('各種アサーション', async ({ page }) => {
await page.goto('/products');
// 可視性
await expect(page.getByTestId('loading')).toBeVisible();
await expect(page.getByTestId('loading')).not.toBeVisible();
// テキスト内容
await expect(page.getByRole('heading')).toHaveText('商品一覧');
await expect(page.getByRole('heading')).toContainText('商品');
// 属性
await expect(page.getByRole('link')).toHaveAttribute('href', '/cart');
// カウント
await expect(page.getByTestId('product-card')).toHaveCount(10);
// 有効/無効
await expect(page.getByRole('button', { name: '購入' })).toBeEnabled();
await expect(page.getByRole('button', { name: '購入' })).toBeDisabled();
// フォーム値
await expect(page.getByLabel('数量')).toHaveValue('1');
});
フォームテスト
入力とサブミット
test.describe('問い合わせフォーム', () => {
test('フォーム送信が成功する', async ({ page }) => {
await page.goto('/contact');
// フォーム入力
await page.getByLabel('お名前').fill('田中太郎');
await page.getByLabel('メールアドレス').fill('tanaka@example.com');
await page.getByLabel('件名').selectOption('inquiry');
await page.getByLabel('メッセージ').fill('お問い合わせ内容をここに記入します。');
// チェックボックス
await page.getByLabel('プライバシーポリシーに同意する').check();
// 送信
await page.getByRole('button', { name: '送信' }).click();
// 成功メッセージの確認
await expect(page.getByRole('alert')).toHaveText('送信が完了しました');
});
test('バリデーションエラーが表示される', async ({ page }) => {
await page.goto('/contact');
// 空のまま送信
await page.getByRole('button', { name: '送信' }).click();
// エラーメッセージの確認
await expect(page.getByText('お名前は必須です')).toBeVisible();
await expect(page.getByText('メールアドレスは必須です')).toBeVisible();
});
});
Page Object Model
大規模なテストでは、Page Object Modelパターンを使用して保守性を向上させます。
Page Object の作成
// pages/login.page.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.getByLabel('メールアドレス');
this.passwordInput = page.getByLabel('パスワード');
this.submitButton = page.getByRole('button', { name: 'ログイン' });
this.errorMessage = page.getByRole('alert');
}
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();
}
}
// pages/dashboard.page.ts
import { Page, Locator } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly welcomeMessage: Locator;
readonly logoutButton: Locator;
readonly userMenu: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeMessage = page.getByTestId('welcome-message');
this.logoutButton = page.getByRole('button', { name: 'ログアウト' });
this.userMenu = page.getByTestId('user-menu');
}
async logout() {
await this.userMenu.click();
await this.logoutButton.click();
}
}
テストでの使用
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';
test.describe('ログイン機能', () => {
test('正しい認証情報でログインできる', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
// ダッシュボードへのリダイレクトを確認
await expect(page).toHaveURL('/dashboard');
await expect(dashboardPage.welcomeMessage).toContainText('ようこそ');
});
test('誤った認証情報でエラーが表示される', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'wrongpassword');
await expect(loginPage.errorMessage).toHaveText('メールアドレスまたはパスワードが正しくありません');
await expect(page).toHaveURL('/login');
});
});
認証状態の管理
認証状態の保存と再利用
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
setup('認証', async ({ page }) => {
// ログイン処理
await page.goto('/login');
await page.getByLabel('メールアドレス').fill('user@example.com');
await page.getByLabel('パスワード').fill('password123');
await page.getByRole('button', { name: 'ログイン' }).click();
// ログイン成功を確認
await expect(page).toHaveURL('/dashboard');
// 認証状態を保存
await page.context().storageState({ path: authFile });
});
// playwright.config.ts
export default defineConfig({
projects: [
// 認証セットアップ
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// 認証済みテスト
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
カスタムフィクスチャ
// tests/fixtures/auth.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../../pages/login.page';
import { DashboardPage } from '../../pages/dashboard.page';
type AuthFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: DashboardPage;
};
export const test = base.extend<AuthFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
authenticatedPage: async ({ page }, use) => {
// 自動ログイン
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
});
export { expect } from '@playwright/test';
// tests/dashboard.spec.ts
import { test, expect } from './fixtures/auth';
test('認証済みユーザーがダッシュボードにアクセスできる', async ({ authenticatedPage }) => {
await expect(authenticatedPage.welcomeMessage).toBeVisible();
});
APIモックとインターセプト
APIレスポンスのモック
test.describe('商品一覧', () => {
test('APIからデータを取得して表示する', async ({ page }) => {
// APIレスポンスをモック
await page.route('**/api/products', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: '商品A', price: 1000 },
{ id: 2, name: '商品B', price: 2000 },
{ id: 3, name: '商品C', price: 3000 },
]),
});
});
await page.goto('/products');
// モックされたデータが表示されることを確認
await expect(page.getByTestId('product-card')).toHaveCount(3);
await expect(page.getByText('商品A')).toBeVisible();
});
test('APIエラー時にエラーメッセージが表示される', async ({ page }) => {
await page.route('**/api/products', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/products');
await expect(page.getByText('データの取得に失敗しました')).toBeVisible();
});
});
リクエストの傍受と検証
test('フォーム送信時に正しいデータがAPIに送信される', async ({ page }) => {
let capturedRequest: any = null;
// リクエストを傍受
await page.route('**/api/contact', async route => {
capturedRequest = route.request().postDataJSON();
await route.fulfill({ status: 200, body: JSON.stringify({ success: true }) });
});
await page.goto('/contact');
await page.getByLabel('お名前').fill('田中太郎');
await page.getByLabel('メールアドレス').fill('tanaka@example.com');
await page.getByRole('button', { name: '送信' }).click();
// 送信されたデータを検証
expect(capturedRequest).toEqual({
name: '田中太郎',
email: 'tanaka@example.com',
});
});
ビジュアルリグレッションテスト
スクリーンショット比較
test('ホームページのスクリーンショット', async ({ page }) => {
await page.goto('/');
// ページ全体のスクリーンショット
await expect(page).toHaveScreenshot('homepage.png');
});
test('コンポーネントのスクリーンショット', async ({ page }) => {
await page.goto('/components');
// 特定の要素のみ
const card = page.getByTestId('feature-card');
await expect(card).toHaveScreenshot('feature-card.png');
});
test('レスポンシブデザイン', async ({ page }) => {
await page.goto('/');
// デスクトップ
await page.setViewportSize({ width: 1280, height: 720 });
await expect(page).toHaveScreenshot('homepage-desktop.png');
// タブレット
await page.setViewportSize({ width: 768, height: 1024 });
await expect(page).toHaveScreenshot('homepage-tablet.png');
// モバイル
await page.setViewportSize({ width: 375, height: 667 });
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
CI/CD統合
GitHub Actions設定
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
path: test-results/
retention-days: 30
デバッグテクニック
UI モードでのデバッグ
# UI モードで実行
npx playwright test --ui
# 特定のテストをデバッグ
npx playwright test --debug tests/login.spec.ts
トレースビューア
// playwright.config.ts
use: {
trace: 'on-first-retry', // 失敗時のみトレース
// trace: 'on', // 常にトレース
}
# トレースファイルを開く
npx playwright show-trace test-results/example-spec-ts/trace.zip
console.log の表示
test('デバッグ出力', async ({ page }) => {
// ブラウザのconsole.logをキャプチャ
page.on('console', msg => console.log('Browser log:', msg.text()));
await page.goto('/');
// Playwrightのpause(デバッガ)
await page.pause();
});
テスト実行コマンド
# 全テスト実行
npx playwright test
# 特定のファイル
npx playwright test tests/login.spec.ts
# 特定のブラウザ
npx playwright test --project=chromium
# ヘッドありモード(ブラウザ表示)
npx playwright test --headed
# 並列数の制御
npx playwright test --workers=4
# 失敗したテストのみ再実行
npx playwright test --last-failed
# タグでフィルタ
npx playwright test --grep @smoke
# HTMLレポートを開く
npx playwright show-report
まとめ
Playwrightを使ったE2Eテストのポイントをまとめます。
ベストプラクティス
- ロケーター: ユーザー視点のロケーター(getByRole, getByLabel)を優先
- Page Object: 大規模テストでは保守性のためPOMを採用
- 認証: storageStateで認証状態を再利用
- APIモック: route()でバックエンドに依存しないテスト
- CI統合: GitHub Actionsで自動テスト
次のステップ
E2Eテストは開発の品質を大きく向上させます。Playwrightの強力な機能を活用して、信頼性の高いテストを構築しましょう。