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のベストプラクティスを学びます。