10日で覚えるPlaywrightDay 2: テストの基本構造
books.chapter 210日で覚えるPlaywright

Day 2: テストの基本構造

今日学ぶこと

  • test() 関数と test.describe() によるグルーピング
  • フック: beforeAll, beforeEach, afterEach, afterAll
  • expect() による基本的なアサーション
  • test.only(), test.skip(), test.fixme() によるテスト制御
  • アノテーションとタグ
  • playwright.config.ts の設定項目
  • CLIからのテスト実行方法
  • テストファイルの命名規則

テストファイルの命名規則

Playwrightのテストファイルは、デフォルトで以下のパターンにマッチするファイルが対象になります。

パターン
*.spec.ts login.spec.ts
*.spec.js login.spec.js
*.test.ts login.test.ts
*.test.js login.test.js

推奨されるディレクトリ構造は以下の通りです。

tests/
├── auth/
│   ├── login.spec.ts
│   └── register.spec.ts
├── dashboard/
│   └── overview.spec.ts
└── settings/
    └── profile.spec.ts

test() 関数の基本

Playwrightのテストは test() 関数で定義します。Cypressの it() に相当するものです。

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

test('ホームページのタイトルを確認する', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
});

test() の引数

test() の第2引数であるコールバック関数は、フィクスチャと呼ばれるオブジェクトを受け取ります。最もよく使うのが page フィクスチャです。

test('pageフィクスチャを使う', async ({ page }) => {
  // page はブラウザの1タブに相当する
  await page.goto('https://example.com');
});

test('複数のフィクスチャを使う', async ({ page, context, browser }) => {
  // context: ブラウザコンテキスト(シークレットウィンドウのような隔離環境)
  // browser: ブラウザインスタンス
});

test.describe() によるグルーピング

test.describe() を使ってテストを論理的にグループ化できます。

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

test.describe('ログインページ', () => {

  test('正しい認証情報でログインできる', async ({ page }) => {
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'user@example.com');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="submit"]');
    await expect(page).toHaveURL('/dashboard');
  });

  test('間違ったパスワードでエラーが表示される', async ({ page }) => {
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'user@example.com');
    await page.fill('[data-testid="password"]', 'wrong');
    await page.click('[data-testid="submit"]');
    await expect(page.locator('.error')).toBeVisible();
  });
});

describe のネスト

test.describe('ユーザー管理', () => {

  test.describe('ログイン', () => {
    test('メールアドレスでログインできる', async ({ page }) => {
      // ...
    });
  });

  test.describe('ユーザー登録', () => {
    test('新規ユーザーを作成できる', async ({ page }) => {
      // ...
    });
  });
});

フック(Hooks)

フックを使って、テストの前後に共通処理を実行できます。

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

test.describe('Todoアプリ', () => {

  test.beforeAll(async () => {
    // テストスイート全体の前に1回だけ実行
    // 例: データベースの初期化
    console.log('テストスイート開始');
  });

  test.beforeEach(async ({ page }) => {
    // 各テストの前に毎回実行
    await page.goto('https://demo.playwright.dev/todomvc');
  });

  test.afterEach(async ({ page }) => {
    // 各テストの後に毎回実行
    // 例: スクリーンショットの保存
  });

  test.afterAll(async () => {
    // テストスイート全体の後に1回だけ実行
    // 例: テストデータのクリーンアップ
    console.log('テストスイート終了');
  });

  test('Todoを追加できる', async ({ page }) => {
    await page.locator('.new-todo').fill('牛乳を買う');
    await page.locator('.new-todo').press('Enter');
    await expect(page.locator('.todo-list li')).toHaveCount(1);
  });
});

フックの実行順序

flowchart TB
    subgraph Lifecycle["テストのライフサイクル"]
        BA["beforeAll()\n1回だけ実行"]
        BE1["beforeEach()\n毎回実行"]
        T1["test('テスト1')"]
        AE1["afterEach()\n毎回実行"]
        BE2["beforeEach()\n毎回実行"]
        T2["test('テスト2')"]
        AE2["afterEach()\n毎回実行"]
        AA["afterAll()\n1回だけ実行"]
    end
    BA --> BE1 --> T1 --> AE1 --> BE2 --> T2 --> AE2 --> AA
    style BA fill:#8b5cf6,color:#fff
    style AA fill:#8b5cf6,color:#fff
    style BE1 fill:#3b82f6,color:#fff
    style BE2 fill:#3b82f6,color:#fff
    style AE1 fill:#f59e0b,color:#fff
    style AE2 fill:#f59e0b,color:#fff
    style T1 fill:#22c55e,color:#fff
    style T2 fill:#22c55e,color:#fff
フック 実行タイミング フィクスチャ 主な用途
test.beforeAll スイートの最初に1回 なし(またはworker scope) DB初期化、サーバー起動
test.beforeEach 各テストの前に毎回 page, context ページ遷移、状態リセット
test.afterEach 各テストの後に毎回 page, context スクリーンショット保存
test.afterAll スイートの最後に1回 なし(またはworker scope) クリーンアップ

注意: beforeAllafterAll では page フィクスチャは利用できません。ワーカーレベルのフィクスチャ(browser など)のみ使えます。


expect() による基本的なアサーション

Playwrightの expect() は自動リトライ機能を持っています。要素が条件を満たすまで一定時間待機してくれます。

ページに対するアサーション

// タイトルの検証
await expect(page).toHaveTitle('My App');
await expect(page).toHaveTitle(/My App/);

// URLの検証
await expect(page).toHaveURL('https://example.com/dashboard');
await expect(page).toHaveURL(/dashboard/);

ロケータに対するアサーション

const button = page.locator('[data-testid="submit"]');

// 表示・存在の検証
await expect(button).toBeVisible();
await expect(button).toBeHidden();
await expect(button).toBeEnabled();
await expect(button).toBeDisabled();

// テキストの検証
await expect(button).toHaveText('送信');
await expect(button).toContainText('送信');

// 属性・CSSの検証
await expect(button).toHaveAttribute('type', 'submit');
await expect(button).toHaveClass(/primary/);
await expect(button).toHaveCSS('color', 'rgb(255, 0, 0)');

// 入力値の検証
await expect(page.locator('input')).toHaveValue('test@example.com');

// 要素数の検証
await expect(page.locator('.todo-item')).toHaveCount(3);

// チェック状態の検証
await expect(page.locator('input[type="checkbox"]')).toBeChecked();

否定のアサーション

not を使って条件を否定できます。

await expect(button).not.toBeDisabled();
await expect(page.locator('.error')).not.toBeVisible();

汎用的なアサーション

ページやロケータ以外の値にも expect() を使えます。

const count = await page.locator('.item').count();
expect(count).toBe(5);
expect(count).toBeGreaterThan(0);

const text = await page.locator('h1').textContent();
expect(text).toContain('Welcome');

主要なアサーション一覧

アサーション 対象 説明
toHaveTitle() Page ページタイトルの検証
toHaveURL() Page URLの検証
toBeVisible() Locator 要素が表示されている
toBeHidden() Locator 要素が非表示
toBeEnabled() Locator 要素が有効
toBeDisabled() Locator 要素が無効
toHaveText() Locator テキストの完全一致
toContainText() Locator テキストの部分一致
toHaveValue() Locator 入力値の検証
toHaveCount() Locator 要素数の検証
toHaveAttribute() Locator 属性の検証
toBeChecked() Locator チェック状態の検証

テスト制御: only, skip, fixme

test.only() - 特定のテストだけ実行

開発中に特定のテストだけを実行したい場合に使います。

test.only('このテストだけ実行される', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
});

test('このテストはスキップされる', async ({ page }) => {
  // ...
});

test.skip() - テストをスキップ

一時的にテストを無効にしたい場合に使います。

test.skip('一時的にスキップするテスト', async ({ page }) => {
  // このテストは実行されない
});

// 条件付きスキップ
test('Firefoxでは実行しない', async ({ page, browserName }) => {
  test.skip(browserName === 'firefox', 'Firefoxでは未対応');
  // ...
});

test.fixme() - 修正予定のテスト

バグがあり将来修正予定のテストをマークします。skip と似ていますが、意図が異なります。

test.fixme('バグ修正後に有効にする', async ({ page }) => {
  // このテストは実行されない
  // 修正が必要なことを明示する
});

使い分けの指針

flowchart LR
    subgraph Only["test.only()"]
        O["開発中の一時的な\nフォーカス実行"]
    end
    subgraph Skip["test.skip()"]
        S["環境依存や\n一時的な無効化"]
    end
    subgraph Fixme["test.fixme()"]
        F["既知のバグで\n修正予定"]
    end
    style Only fill:#3b82f6,color:#fff
    style Skip fill:#f59e0b,color:#fff
    style Fixme fill:#ef4444,color:#fff

注意: test.only() をコミットに含めないよう注意しましょう。CI/CDで他のテストが実行されなくなります。


アノテーションとタグ

タグによるテストの分類

テストにタグを付けて、実行時にフィルタリングできます。

test('ログインできる @smoke', async ({ page }) => {
  // --grep @smoke で実行可能
});

test('全商品を一覧表示できる @regression', async ({ page }) => {
  // --grep @regression で実行可能
});

test('重い処理のテスト @slow', async ({ page }) => {
  // --grep @slow で実行可能
});

test.describe.configure()

describe ブロックの実行モードを設定できます。

test.describe('順序に依存するテスト', () => {
  test.describe.configure({ mode: 'serial' });

  test('ステップ1: ユーザーを作成', async ({ page }) => {
    // ...
  });

  test('ステップ2: ユーザーでログイン', async ({ page }) => {
    // ...
  });
});
モード 説明
parallel テストを並列実行(デフォルト)
serial テストを順次実行。1つ失敗すると残りはスキップ

test.slow()

テストのタイムアウトを3倍に延長します。

test('大量データの読み込みテスト', async ({ page }) => {
  test.slow();
  // タイムアウトが3倍になる
  await page.goto('/large-data');
  await expect(page.locator('.data-table')).toBeVisible();
});

playwright.config.ts の設定

Playwrightの動作はプロジェクトルートの playwright.config.ts で制御します。

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

export default defineConfig({
  // テストファイルのディレクトリ
  testDir: './tests',

  // テストファイルのマッチパターン
  testMatch: '**/*.spec.ts',

  // 全テストの最大実行時間(ミリ秒)
  timeout: 30000,

  // expect() のタイムアウト
  expect: {
    timeout: 5000,
  },

  // テスト失敗時のリトライ回数
  retries: process.env.CI ? 2 : 0,

  // 並列ワーカー数
  workers: process.env.CI ? 1 : undefined,

  // テストの実行順序を完全並列にする
  fullyParallel: true,

  // test.only() がある場合にCIで失敗させる
  forbidOnly: !!process.env.CI,

  // レポーター設定
  reporter: [
    ['html', { open: 'never' }],
    ['list'],
  ],

  // 全プロジェクト共通の設定
  use: {
    // ベースURL
    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'] },
    },
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],

  // テスト実行前にローカルサーバーを起動
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

主要な設定項目

flowchart TB
    subgraph Config["playwright.config.ts"]
        subgraph TestExec["テスト実行"]
            TD["testDir\nテストディレクトリ"]
            TO["timeout\nタイムアウト"]
            RT["retries\nリトライ回数"]
            WK["workers\n並列数"]
        end
        subgraph UseBlock["use(共通設定)"]
            BU["baseURL"]
            TR["trace"]
            SS["screenshot"]
        end
        subgraph Proj["projects(ブラウザ)"]
            CR["Chromium"]
            FF["Firefox"]
            WB["WebKit"]
        end
        subgraph Rep["reporter"]
            RP["html / list / json"]
        end
    end
    style TestExec fill:#3b82f6,color:#fff
    style UseBlock fill:#22c55e,color:#fff
    style Proj fill:#8b5cf6,color:#fff
    style Rep fill:#f59e0b,color:#fff

CLIからのテスト実行

基本的な実行コマンド

# 全テストを実行
npx playwright test

# 特定のファイルを実行
npx playwright test tests/login.spec.ts

# 特定のディレクトリを実行
npx playwright test tests/auth/

# テスト名でフィルタリング(正規表現)
npx playwright test --grep "ログイン"

# 特定のテストを除外
npx playwright test --grep-invert "スロー"

プロジェクト(ブラウザ)指定

# Chromiumのみ
npx playwright test --project=chromium

# 複数ブラウザ
npx playwright test --project=chromium --project=firefox

デバッグ・開発向けオプション

# ブラウザを表示して実行(headed モード)
npx playwright test --headed

# デバッガー付きで実行(ステップ実行可能)
npx playwright test --debug

# UIモードで実行(テストの可視化)
npx playwright test --ui

# 最後に失敗したテストだけ再実行
npx playwright test --last-failed

レポート・出力

# HTMLレポートを表示
npx playwright show-report

# レポーターを指定して実行
npx playwright test --reporter=list
npx playwright test --reporter=dot
npx playwright test --reporter=json

便利なオプション一覧

オプション 説明
--headed ブラウザを表示して実行 npx playwright test --headed
--debug デバッガー起動 npx playwright test --debug
--ui UIモード起動 npx playwright test --ui
--project ブラウザ指定 --project=chromium
--grep テスト名フィルタ --grep "login"
--grep-invert テスト名除外 --grep-invert "slow"
--workers 並列数指定 --workers=4
--retries リトライ回数 --retries=2
--reporter レポーター指定 --reporter=html
--last-failed 前回失敗分のみ --last-failed

実践:テスト構造の全体像

ここまで学んだ内容を組み合わせた実践的なテストファイルを見てみましょう。

// tests/todo.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Todoアプリ', () => {

  test.beforeEach(async ({ page }) => {
    await page.goto('https://demo.playwright.dev/todomvc');
  });

  test.describe('Todo追加', () => {

    test('新しいTodoを追加できる @smoke', async ({ page }) => {
      const input = page.locator('.new-todo');
      await input.fill('牛乳を買う');
      await input.press('Enter');

      const items = page.locator('.todo-list li');
      await expect(items).toHaveCount(1);
      await expect(items.first()).toHaveText('牛乳を買う');
    });

    test('複数のTodoを追加できる', async ({ page }) => {
      const input = page.locator('.new-todo');

      await input.fill('牛乳を買う');
      await input.press('Enter');
      await input.fill('パンを買う');
      await input.press('Enter');
      await input.fill('卵を買う');
      await input.press('Enter');

      await expect(page.locator('.todo-list li')).toHaveCount(3);
    });
  });

  test.describe('Todo完了', () => {

    test.beforeEach(async ({ page }) => {
      // 事前にTodoを2つ追加
      const input = page.locator('.new-todo');
      await input.fill('牛乳を買う');
      await input.press('Enter');
      await input.fill('パンを買う');
      await input.press('Enter');
    });

    test('Todoを完了状態にできる', async ({ page }) => {
      const firstTodo = page.locator('.todo-list li').first();
      await firstTodo.locator('.toggle').check();
      await expect(firstTodo).toHaveClass(/completed/);
    });

    test.skip('完了したTodoを削除できる', async ({ page }) => {
      // TODO: 削除機能のテストは後日実装
    });
  });
});

まとめ

概念 説明
test() テストケースを定義する関数
test.describe() テストをグループ化するブロック
test.beforeEach 各テストの前に実行される共通処理
test.beforeAll スイートの最初に1回だけ実行
expect() 自動リトライ付きのアサーション
test.only() 特定のテストだけを実行
test.skip() テストを一時的にスキップ
test.fixme() 修正予定のテストをマーク
playwright.config.ts テスト全体の設定ファイル
npx playwright test CLIからテストを実行

重要ポイント

  1. test.beforeEach で各テストの事前条件を整え、テスト間の独立性を保つ
  2. expect() は自動リトライ機能を持ち、非同期なUI変化にも自然に対応できる
  3. playwright.config.tsprojects で複数ブラウザを一度にテストできる
  4. test.only() はローカル開発用。CIでは forbidOnly: true で防止する
  5. タグ(@smoke, @regression)と --grep を組み合わせて、テスト実行を柔軟に制御する

練習問題

問題1: 基本

以下の要件を満たすテストファイルを作成してください。

  • test.describe() で「ホームページ」というグループを作る
  • test.beforeEach でトップページ(/)に遷移する
  • テスト1: ページタイトルに「Welcome」が含まれることを検証
  • テスト2: ナビゲーションバーが表示されていることを検証

問題2: 応用

以下の playwright.config.ts を作成してください。

  • テストディレクトリ: ./e2e
  • タイムアウト: 60秒
  • ベースURL: http://localhost:8080
  • プロジェクト: Chromium と Firefox の2つ
  • CI環境ではリトライを3回に設定
  • HTMLレポーターを使用

チャレンジ問題

以下の要件を満たすテストファイルを作成してください。

  • TodoMVCアプリ(https://demo.playwright.dev/todomvc)を対象とする
  • @smoke タグ付きのテスト: Todoを1つ追加して表示を確認
  • @regression タグ付きのテスト: Todoを5つ追加し、3つを完了にして、残り件数が「2 items left」であることを確認
  • test.describe.configure({ mode: 'serial' }) を使って順序を保証する

参考リンク


次回予告: Day 3では「ロケータとDOM操作」について学びます。Playwrightのロケータ戦略を理解し、堅牢な要素選択の方法を習得しましょう。