10日で覚えるPlaywrightDay 9: 並列実行とパフォーマンス
books.chapter 910日で覚えるPlaywright

Day 9: 並列実行とパフォーマンス

今日学ぶこと

  • Playwrightの並列実行モデル(Worker)
  • workers オプションと fullyParallel モード
  • test.describe.serial() による逐次実行
  • CI向けのシャーディング
  • リトライとフレーキーテスト対策
  • タイムアウトの種類と設定
  • テスト実行のパフォーマンス最適化

Playwrightの並列実行モデル

Playwrightはデフォルトでテストファイルを並列に実行します。各テストファイルは独立したWorker プロセスで実行され、テスト間の干渉を防ぎます。

flowchart TB
    subgraph Runner["Test Runner"]
        R["playwright test"]
    end

    subgraph Workers["Worker プロセス"]
        W1["Worker 1<br/>login.spec.ts"]
        W2["Worker 2<br/>cart.spec.ts"]
        W3["Worker 3<br/>search.spec.ts"]
    end

    subgraph Browsers["ブラウザインスタンス"]
        B1["Chromium"]
        B2["Chromium"]
        B3["Chromium"]
    end

    R --> W1 & W2 & W3
    W1 --> B1
    W2 --> B2
    W3 --> B3

    style Runner fill:#3b82f6,color:#fff
    style Workers fill:#8b5cf6,color:#fff
    style Browsers fill:#22c55e,color:#fff

重要なポイント:

  • 1つのWorkerは1つのテストファイルを担当する
  • 同じファイル内のテストはデフォルトで逐次実行される
  • Worker同士は完全に独立しており、状態を共有しない

Workers の設定

playwright.config.ts での設定

import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Worker数を指定(デフォルトはCPUコアの半分)
  workers: 4,

  // CI環境では1に制限するパターン
  // workers: process.env.CI ? 1 : undefined,
});

CLI での指定

# Worker数を指定
npx playwright test --workers=4

# CPU使用率で指定(50%)
npx playwright test --workers=50%

# 並列実行を無効化(デバッグ時に便利)
npx playwright test --workers=1

Workers 数の目安

環境 推奨 Workers 数 理由
ローカル開発 CPU コアの半分(デフォルト) 開発作業と並行できる
CI(小規模) 1-2 メモリ制限に注意
CI(大規模) 4-8 リソースに応じて調整
デバッグ 1 出力を見やすくする

fullyParallel モード

デフォルトでは、同じファイル内のテストは逐次実行されます。fullyParallel を有効にすると、ファイル内のテストも並列実行されます。

import { defineConfig } from '@playwright/test';

export default defineConfig({
  // プロジェクト全体で有効化
  fullyParallel: true,
  workers: 4,
});

特定の describe ブロックだけ並列にすることもできます:

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

// このブロック内のテストは並列実行される
test.describe.configure({ mode: 'parallel' });

test('test A', async ({ page }) => {
  // ...
});

test('test B', async ({ page }) => {
  // ...
});

fullyParallel を有効にする条件

  • 各テストが完全に独立している
  • テスト間でデータの依存関係がない
  • 各テストが自分自身でセットアップ・クリーンアップを行う

test.describe.serial() による逐次実行

テストが順序に依存する場合は serial を使います。前のテストが失敗すると、後続のテストはスキップされます。

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

test.describe.serial('注文フロー', () => {
  test('Step 1: 商品をカートに追加', async ({ page }) => {
    await page.goto('/products/1');
    await page.click('button:text("カートに追加")');
    await expect(page.locator('.cart-count')).toHaveText('1');
  });

  test('Step 2: チェックアウト', async ({ page }) => {
    await page.goto('/cart');
    await page.click('button:text("購入手続きへ")');
    await expect(page).toHaveURL(/\/checkout/);
  });

  test('Step 3: 注文確認', async ({ page }) => {
    await page.goto('/checkout');
    await page.fill('#card-number', '4242424242424242');
    await page.click('button:text("注文する")');
    await expect(page.locator('.order-confirmation')).toBeVisible();
  });
});

注意: serial は可能な限り避け、各テストを独立させることが推奨されます。テスト間で状態を共有する必要がある場合にのみ使用してください。


CI向けのシャーディング

大規模なテストスイートでは、シャーディングでテストを複数のCI マシンに分散できます。

# 3台のマシンに分散する場合
# マシン1
npx playwright test --shard=1/3

# マシン2
npx playwright test --shard=2/3

# マシン3
npx playwright test --shard=3/3

GitHub Actions でのシャーディング例

name: Playwright Tests
on: [push]

jobs:
  test:
    strategy:
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test --shard=${{ matrix.shard }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: report-${{ strategy.job-index }}
          path: playwright-report/
flowchart LR
    subgraph CI["CI パイプライン"]
        Push["git push"] --> Split["テスト分割"]
        Split --> S1["Shard 1/4<br/>25テスト"]
        Split --> S2["Shard 2/4<br/>25テスト"]
        Split --> S3["Shard 3/4<br/>25テスト"]
        Split --> S4["Shard 4/4<br/>25テスト"]
        S1 & S2 & S3 & S4 --> Merge["結果マージ"]
    end

    style CI fill:#3b82f6,color:#fff

リトライの設定

テストのフレーキーさ(不安定さ)に対処するため、失敗時にリトライできます。

グローバル設定

import { defineConfig } from '@playwright/test';

export default defineConfig({
  // 全テストで最大2回リトライ
  retries: 2,

  // CI環境のみリトライ
  // retries: process.env.CI ? 2 : 0,
});

CLI での指定

npx playwright test --retries=2

テスト内でのリトライ情報の利用

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

test('リトライ情報を使うテスト', async ({ page }, testInfo) => {
  // 現在のリトライ回数
  console.log(`Attempt: ${testInfo.retry + 1}`);

  // リトライ時に追加のログを出力
  if (testInfo.retry > 0) {
    console.log('Retrying - enabling verbose logging');
  }

  await page.goto('/');
  await expect(page.locator('h1')).toBeVisible();
});

フレーキーテストの検出

リトライで成功したテストは、レポートで flaky としてマークされます。

# テストを実行してレポートを確認
npx playwright test
npx playwright show-report

フレーキーテストの一般的な原因と対策

原因 対策
タイミングの問題 waitFor やアサーションの自動リトライに任せる
テストデータの競合 各テストで固有のデータを使う
アニメーション page.emulateMedia やCSS無効化
ネットワーク不安定 リクエストのモック
外部サービス依存 API モックを活用

タイムアウトの種類と設定

Playwrightには複数のタイムアウトがあります。それぞれの役割を理解することが重要です。

flowchart TB
    subgraph Timeouts["タイムアウトの種類"]
        GT["Global Timeout<br/>全テスト合計"]
        TT["Test Timeout<br/>個別テスト<br/>デフォルト: 30秒"]
        AT["Action Timeout<br/>click, fill など"]
        NT["Navigation Timeout<br/>goto, reload など"]
        ET["Expect Timeout<br/>アサーション<br/>デフォルト: 5秒"]
    end

    GT --> TT --> AT & NT & ET

    style Timeouts fill:#f59e0b,color:#fff

設定方法

import { defineConfig } from '@playwright/test';

export default defineConfig({
  // テスト全体のタイムアウト(デフォルト: 無制限)
  globalTimeout: 60 * 60 * 1000, // 1時間

  // 個別テストのタイムアウト(デフォルト: 30秒)
  timeout: 60_000, // 60秒

  // expect のタイムアウト
  expect: {
    timeout: 10_000, // 10秒
  },

  use: {
    // アクションのタイムアウト
    actionTimeout: 15_000, // 15秒

    // ナビゲーションのタイムアウト
    navigationTimeout: 30_000, // 30秒
  },
});

テスト単位でのタイムアウト設定

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

// テスト単位で設定
test('重い処理のテスト', async ({ page }) => {
  test.setTimeout(120_000); // 2分

  await page.goto('/heavy-page');
  await expect(page.locator('.loaded')).toBeVisible();
});

// slow() で3倍に延長
test('遅いテスト', async ({ page }) => {
  test.slow(); // タイムアウトを3倍に

  await page.goto('/slow-page');
  // ...
});

パフォーマンス最適化

1. 認証状態の再利用

毎回ログインするのではなく、認証状態を保存して再利用します。

// auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('/dashboard');

  // 認証状態を保存
  await page.context().storageState({ path: authFile });
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';

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

2. 不要なリソースのブロック

画像やフォントなど、テストに不要なリソースをブロックして高速化します。

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

test('不要なリソースをブロック', async ({ page }) => {
  // 画像・フォント・CSSをブロック
  await page.route('**/*.{png,jpg,jpeg,gif,svg,woff,woff2}', (route) => {
    route.abort();
  });

  // サードパーティスクリプトをブロック
  await page.route('**/analytics.js', (route) => route.abort());
  await page.route('**/ads/**', (route) => route.abort());

  await page.goto('/');
  await expect(page.locator('h1')).toBeVisible();
});

3. グローバルセットアップでの共通処理

// global-setup.ts
import { chromium } from '@playwright/test';

async function globalSetup() {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // テストデータの準備
  await page.goto('/api/test/seed');

  await browser.close();
}

export default globalSetup;
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
});

4. テスト実行時間の計測

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

test('パフォーマンス計測', async ({ page }, testInfo) => {
  const start = Date.now();

  await page.goto('/');
  await expect(page.locator('.content')).toBeVisible();

  const duration = Date.now() - start;
  console.log(`Page load took ${duration}ms`);

  // テスト結果にメタデータとして記録
  testInfo.annotations.push({
    type: 'performance',
    description: `Load time: ${duration}ms`,
  });
});

5. パフォーマンス最適化チェックリスト

手法 効果 実装の難易度
認証状態の再利用 大 低
不要リソースのブロック 中 低
シャーディング 大 中
fullyParallel 中 低
API モックの活用 中 中
グローバルセットアップ 中 低

まとめ

今日は並列実行とパフォーマンス最適化について学びました。

概念 説明
Workers テストファイルを並列実行するプロセス
fullyParallel ファイル内のテストも並列化
serial テストの逐次実行を保証
シャーディング テストをCI マシンに分散
リトライ 失敗テストの自動再実行
タイムアウト テスト・アクション・ナビゲーション・expectそれぞれに設定可能
認証状態再利用 storageState で毎回のログインを省略

次回予告: Day 10 では、CI/CDパイプラインへの統合とPlaywrightのベストプラクティスを学びます。