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) | クリーンアップ |
注意:
beforeAllとafterAllでは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からテストを実行 |
重要ポイント
test.beforeEachで各テストの事前条件を整え、テスト間の独立性を保つexpect()は自動リトライ機能を持ち、非同期なUI変化にも自然に対応できるplaywright.config.tsのprojectsで複数ブラウザを一度にテストできるtest.only()はローカル開発用。CIではforbidOnly: trueで防止する- タグ(
@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' })を使って順序を保証する
参考リンク
- Playwright公式 - Writing Tests
- Playwright公式 - Test Configuration
- Playwright公式 - Annotations
- Playwright公式 - Command Line
- Playwright公式 - Assertions
次回予告: Day 3では「ロケータとDOM操作」について学びます。Playwrightのロケータ戦略を理解し、堅牢な要素選択の方法を習得しましょう。