Playwright 2025 - モダンE2Eテスト自動化フレームワークの最新活用

2026.04.10

公式ドキュメント

この記事の要点

• Chromium/Firefox/WebKitを統一APIで操作可能な最新E2Eテストフレームワーク
• 自動待機(auto-waiting)とTrace Viewerでflakyテストの削減とデバッグが劇的に改善
• API Testing、Component Testing、視覚回帰テストまで一つのツールでカバー

Playwright は Microsoft が開発する、Chromium / Firefox / WebKit すべてをサポートする E2E テスト自動化フレームワークです。2025 年現在、Cypress や Selenium を上回る勢いで採用が広がり、モダン Web 開発のデファクトスタンダードになりつつあります。本記事では 2025 年時点の Playwright の主要機能と実践的な使い方をまとめます。

Playwrightの概要

アーキテクチャ

flowchart TB
    Test["Test file (TS)"] --> Runner["@playwright/test runner"]
    Runner --> Fixture["Fixture / Worker"]
    Fixture --> Browser["Browser (Chromium/Firefox/WebKit)"]
    Browser --> Page["Page / BrowserContext"]
    Page --> Locator["Locator API"]
    Page --> Trace["Trace Viewer"]
    Page --> Screenshot["Screenshot / Video"]

なぜ Playwright か

  • 3 つのブラウザエンジンを統一 API で操作可能
  • 自動待機 (auto-waiting) による flaky テストの削減
  • Trace Viewer によるデバッグ体験
  • 並列実行とシャーディング
  • Component Testing / API Testing の組み込み
  • TypeScript ファーストな API

主要機能詳細

1. Locator API

Playwright の中心的な API で、ユーザ視点のセレクタを推奨します。

import { test, expect } from '@playwright/test';

test('login flow', async ({ page }) => {
  await page.goto('https://example.com/login');

  await page.getByLabel('Email').fill('alice@example.com');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

推奨順:

  1. getByRole - アクセシビリティツリー経由
  2. getByLabel / getByPlaceholder
  3. getByText
  4. getByTestId - 他が使えないとき

2. 自動待機

// 要素がvisible & enabled & stable になるまで自動で待機
await page.getByRole('button', { name: 'Submit' }).click();

// expect もretry付き
await expect(page.getByText('Saved')).toBeVisible({ timeout: 5000 });

3. Trace Viewer

# 実行時にトレースを保存
npx playwright test --trace on

# トレースを開く
npx playwright show-trace trace.zip

ポイント: Trace ViewerではDOMスナップショット、ネットワーク、コンソール、アクションログが時系列で参照でき、デバッグが劇的に楽になります。

Trace Viewer では DOM スナップショット、ネットワーク、コンソール、アクションログが時系列で参照でき、失敗したテストのデバッグが劇的に楽になります

4. API Testing

import { test, expect } from '@playwright/test';

test('create user via API', async ({ request }) => {
  const res = await request.post('/api/users', {
    data: { name: 'Bob', email: 'bob@example.com' },
  });
  expect(res.ok()).toBeTruthy();

  const body = await res.json();
  expect(body).toMatchObject({ name: 'Bob' });
});

5. Component Testing

// Button.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';

test('button renders and clicks', async ({ mount }) => {
  let clicked = false;
  const component = await mount(
    <Button onClick={() => { clicked = true; }}>Save</Button>,
  );
  await expect(component).toContainText('Save');
  await component.click();
  expect(clicked).toBe(true);
});

実践サンプル

Page Object Model

// pages/LoginPage.ts
import type { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });
  }

  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/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test.describe('auth', () => {
  test('successful login', async ({ page }) => {
    const login = new LoginPage(page);
    await login.goto();
    await login.login('alice@example.com', 'secret');
    await expect(page).toHaveURL(/\/dashboard/);
  });
});

認証状態の再利用

// 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.getByLabel('Email').fill(process.env.TEST_EMAIL!);
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('/dashboard');
  await page.context().storageState({ path: authFile });
});
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: {
        ...devices['Desktop Firefox'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

視覚回帰テスト

import { test, expect } from '@playwright/test';

test('homepage visual', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixelRatio: 0.01,
  });
});

test('hero component', async ({ page }) => {
  await page.goto('/');
  const hero = page.getByTestId('hero');
  await expect(hero).toHaveScreenshot('hero.png');
});

ネットワークモック

test('mock API', async ({ page }) => {
  await page.route('**/api/users', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
    });
  });

  await page.goto('/users');
  await expect(page.getByText('Mock User')).toBeVisible();
});

CI 設定 (GitHub Actions)

name: Playwright Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install deps
        run: pnpm install --frozen-lockfile

      - name: Install browsers
        run: pnpm exec playwright install --with-deps

      - name: Run tests
        run: pnpm exec playwright test --shard=${{ matrix.shard }}/4

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report-${{ matrix.shard }}
          path: playwright-report/
          retention-days: 7

比較表

E2E テストツール比較

項目PlaywrightCypressSelenium
対応ブラウザChromium/Firefox/WebKitChromium系中心ほぼ全て
多タブ/複数オリジンネイティブ対応制限あり対応
自動待機ありありなし
API テストありあり (plugin)なし
Component Testあり (experimental)ありなし
並列実行ネイティブ有料版外部ツール
デバッグツールTrace ViewerDashboardなし

ベストプラクティス

1. ユーザ視点のセレクタ

CSS セレクタや XPath ではなく getByRole を優先します。アクセシビリティも同時に検証できます。

2. テストを独立させる

各テストは他のテストに依存せず、独立してパスするように書きます。test.describe.serial は最後の手段です。

3. test.step で可読性を上げる

test('checkout', async ({ page }) => {
  await test.step('add item', async () => {
    await page.getByRole('button', { name: 'Add to cart' }).click();
  });
  await test.step('go to cart', async () => {
    await page.getByRole('link', { name: 'Cart' }).click();
  });
});

4. 環境変数で環境分離

use: {
  baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
}

5. Fixtures を活用する

繰り返されるセットアップは custom fixture にまとめます。

import { test as base } from '@playwright/test';

type MyFixtures = {
  authedPage: Page;
};

export const test = base.extend<MyFixtures>({
  authedPage: async ({ page }, use) => {
    await page.goto('/login');
    // ログイン処理
    await use(page);
  },
});

注意点

注意: 視覚回帰テストはOS/フォント/GPUの違いで差分が出やすいため、CI上で生成したベースライン画像をコミットするのが基本です。

  • 視覚回帰テストは OS / フォント / GPU の違いで差分が出やすいため、CI 上で生成したベースライン画像をコミットするのが基本です。
  • page.waitForTimeout は原則使わず、expect のリトライか waitFor を使います。
  • 実行時のブラウザバイナリは playwright install で別途取得する必要があります。CI では毎回インストールするかキャッシュを検討しましょう。
  • WebKit は macOS 以外では Linux ビルドが使われるため、Safari 実機と微妙に差が出る場合があります。
  • 大量のテストを並列実行するとマシンリソースを食い尽くすので、workers とシャードを適切に設定します。

導入手順

# 新規導入
pnpm create playwright

# 既存プロジェクトに追加
pnpm add -D @playwright/test
pnpm exec playwright install
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [['html'], ['list']],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
  webServer: {
    command: 'pnpm dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

パフォーマンス

ワーカー数とシャード

# ローカル: CPUコア数の半分
npx playwright test --workers=4

# CI: シャードで物理マシン分割
npx playwright test --shard=1/4

storageState によるログイン短縮

実践メモ: ログインはsetupプロジェクトで一度だけ実行し、storageStateで再利用するとテスト実行時間を大幅に削減できます。

ログインは setup プロジェクトで一度だけ実行し、各テストは保存済みの状態を再利用することで秒単位で実行時間を削減できます。

テスト隔離と再利用のバランス

test.describe.parallel でテスト内のブロックを並列化、test.describe.configure({ mode: 'serial' }) で直列化を選べます。

FAQ

Q: Cypress から移行する価値はありますか? A: クロスブラウザや多タブ、API テストの要件があれば価値は大きいです。既存プロジェクトが安定しているなら急ぐ必要はありません。

Q: テストが flaky です、どうすれば? A: waitForTimeoutexpect(locator).toBeVisible() に置き換え、Trace Viewer で原因を特定します。

Q: モバイルブラウザのテストは? A: devices['iPhone 14'] などでエミュレーションできますが、実機は別途クラウドサービスを使うのが一般的です。

Q: 認証情報をどう扱う? A: .env.test と GitHub Secrets を使い、テスト用の専用アカウントを用意します。

Q: 1 テストあたりの理想的な時間は? A: 10 秒以内を目安にし、それ以上は並列化やセットアップの見直しを検討します。

まとめ

Playwright はモダン E2E テストの決定版と言える完成度を持ち、クロスブラウザ対応、Trace Viewer による強力なデバッグ体験、API / Component テストまで一つのツールでカバーできます。自動待機による flaky テストの削減、Page Object Model、Fixture、ネットワークモックといった機能を組み合わせれば、保守性の高いテストスイートを構築できます。E2E テスト基盤を新しく立ち上げるなら、まず Playwright を検討することを強く推奨します。

参考リソース

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る