テストケースの書き方
テストケースの書き方
良いテストケースを書くことは、効果的なテスト戦略の基盤です。このチャプターでは、テストケースの構造、Given-When-Then パターン、そして実践的な例について学びます。
テストケースの基本構造
テストケースは、明確な構造を持つことで読みやすく、メンテナンスしやすくなります。
3つのフェーズ
すべてのテストケースは、以下の3つのフェーズで構成されます:
| フェーズ | 説明 | 英語表記 |
|---|---|---|
| 前提条件 | テストを実行する前の準備 | Arrange / Given |
| 実行 | テスト対象の操作を実行 | Act / When |
| 検証 | 期待する結果を確認 | Assert / Then |
これを「AAA パターン」(Arrange-Act-Assert)または「Given-When-Then パターン」と呼びます。
Given-When-Then パターン
パターンの概要
Given(前提条件): テストの初期状態を設定
When(実行): 特定の操作を実行
Then(検証): 期待する結果を確認
シンプルな例
import { test, expect } from '@playwright/test';
test('ログイン成功時にダッシュボードが表示される', async ({ page }) => {
// Given: ログインページにアクセス
await page.goto('https://example.com/login');
// When: 正しい認証情報でログイン
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'correctPassword');
await page.click('button[type="submit"]');
// Then: ダッシュボードが表示される
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('h1')).toContainText('ダッシュボード');
});
[!INFO] コメントの活用
Given-When-Thenをコメントで明示することで、テストの意図が明確になります。特に複雑なテストでは、各フェーズを明確に分けることが重要です。
前提条件(Given / Arrange)
目的
テストを実行するための環境や状態を準備します。
前提条件の例
import { test, expect } from '@playwright/test';
test('カートから商品を削除できる', async ({ page }) => {
// Given: ユーザーがログイン済み
await page.goto('https://example.com/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
// Given: カートに商品が追加されている
await page.goto('https://example.com/products/123');
await page.click('#add-to-cart');
await page.goto('https://example.com/cart');
await expect(page.locator('.cart-item')).toHaveCount(1);
// When: カートから商品を削除
await page.click('.remove-item');
// Then: カートが空になる
await expect(page.locator('.cart-empty')).toBeVisible();
await expect(page.locator('.cart-item')).toHaveCount(0);
});
前提条件の最適化
前提条件が複雑な場合、フィクスチャやヘルパー関数で整理できます:
import { test as base, expect } from '@playwright/test';
// カスタムフィクスチャ
const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// Given: 認証済みの状態を準備
await page.goto('https://example.com/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
await use(page);
},
});
// テストがシンプルに
test('プロフィール情報を変更できる', async ({ authenticatedPage }) => {
// When: プロフィール編集ページへ移動
await authenticatedPage.goto('https://example.com/profile/edit');
await authenticatedPage.fill('#name', '新しい名前');
await authenticatedPage.click('#save-button');
// Then: 変更が保存される
await expect(authenticatedPage.locator('#success-message')).toBeVisible();
});
実行(When / Act)
目的
テスト対象の機能を実際に操作します。
実行フェーズの例
test('検索機能でキーワードに一致する商品が表示される', async ({ page }) => {
// Given: トップページにアクセス
await page.goto('https://example.com');
// When: 「ノートPC」で検索
await page.fill('#search-input', 'ノートPC');
await page.click('#search-button');
// Then: 検索結果が表示される
await expect(page.locator('.search-results')).toBeVisible();
await expect(page.locator('.product-card')).not.toHaveCount(0);
});
実行フェーズの注意点
- 1つのテストで1つの操作:複数の操作を1つのテストに詰め込まない
- 明確な操作:何をテストしているのか一目でわかるように
- 適切な待機:操作後の状態変化を適切に待つ
// ❌ 悪い例:複数の操作を1つのテストに
test('商品管理機能', async ({ page }) => {
await page.click('#add-product'); // 商品追加
await page.click('#edit-product'); // 商品編集
await page.click('#delete-product'); // 商品削除
// 何をテストしているのか不明確
});
// ✅ 良い例:1つのテストで1つの操作
test('商品を追加できる', async ({ page }) => {
await page.click('#add-product');
await expect(page.locator('#product-list')).toContainText('新しい商品');
});
test('商品を編集できる', async ({ page }) => {
await page.click('#edit-product');
await expect(page.locator('#edit-form')).toBeVisible();
});
検証(Then / Assert)
目的
期待する結果が得られたかを確認します。
検証の例
test('フォーム送信後に成功メッセージが表示される', async ({ page }) => {
// Given: お問い合わせフォームにアクセス
await page.goto('https://example.com/contact');
// When: フォームに入力して送信
await page.fill('#name', '山田太郎');
await page.fill('#email', 'yamada@example.com');
await page.fill('#message', 'お問い合わせ内容');
await page.click('#submit-button');
// Then: 成功メッセージが表示される
await expect(page.locator('#success-message')).toBeVisible();
await expect(page.locator('#success-message')).toContainText('送信が完了しました');
// Then: フォームがリセットされる
await expect(page.locator('#name')).toHaveValue('');
await expect(page.locator('#email')).toHaveValue('');
await expect(page.locator('#message')).toHaveValue('');
});
検証のベストプラクティス
// ✅ 良い例:複数の観点で検証
test('ログインエラー時の表示', async ({ page }) => {
await page.goto('https://example.com/login');
await page.fill('#email', 'invalid@example.com');
await page.fill('#password', 'wrongpassword');
await page.click('button[type="submit"]');
// Then: エラーメッセージが表示される
await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toContainText('認証に失敗しました');
// Then: URLが変わらない(ログインページに留まる)
await expect(page).toHaveURL('https://example.com/login');
// Then: ログイン状態になっていない
await expect(page.locator('#user-menu')).not.toBeVisible();
});
[!TIP] 適切な検証の数
検証は多すぎても少なすぎてもいけません。テストの目的に合わせて、必要十分な検証を行いましょう。一般的には、1つのテストで2〜5個の検証が適切です。
具体的なテストケースの例
例1: 商品カート機能
import { test, expect } from '@playwright/test';
test.describe('商品カート', () => {
test('商品をカートに追加すると、カート数が増える', async ({ page }) => {
// Given: 商品詳細ページにアクセス
await page.goto('https://example.com/products/laptop-123');
// Given: カートが空の状態
await expect(page.locator('#cart-count')).toContainText('0');
// When: 「カートに追加」ボタンをクリック
await page.click('#add-to-cart');
// Then: カート数が1になる
await expect(page.locator('#cart-count')).toContainText('1');
// Then: 成功メッセージが表示される
await expect(page.locator('.toast-message')).toContainText('カートに追加しました');
});
test('カート内の商品数量を変更できる', async ({ page }) => {
// Given: カートに商品が1つ入っている
await page.goto('https://example.com/products/laptop-123');
await page.click('#add-to-cart');
await page.goto('https://example.com/cart');
// When: 数量を2に変更
await page.fill('.quantity-input', '2');
await page.click('.update-quantity');
// Then: 小計が2倍になる
await expect(page.locator('.item-subtotal')).toContainText('200,000円');
// Then: 合計金額も更新される
await expect(page.locator('.cart-total')).toContainText('200,000円');
});
});
例2: ユーザー登録フォーム
test.describe('ユーザー登録', () => {
test('有効な情報で登録できる', async ({ page }) => {
// Given: 登録フォームにアクセス
await page.goto('https://example.com/register');
// When: 有効な情報を入力
await page.fill('#username', 'newuser123');
await page.fill('#email', 'newuser@example.com');
await page.fill('#password', 'SecurePass123!');
await page.fill('#password-confirm', 'SecurePass123!');
await page.check('#terms-agreement');
await page.click('#register-button');
// Then: 登録完了ページに遷移
await expect(page).toHaveURL(/.*registration-complete/);
await expect(page.locator('h1')).toContainText('登録完了');
// Then: ウェルカムメッセージが表示される
await expect(page.locator('.welcome-message')).toContainText('newuser123さん、ようこそ');
});
test('パスワードが一致しない場合、エラーが表示される', async ({ page }) => {
// Given: 登録フォームにアクセス
await page.goto('https://example.com/register');
// When: パスワードと確認用パスワードが異なる
await page.fill('#username', 'newuser123');
await page.fill('#email', 'newuser@example.com');
await page.fill('#password', 'Password123!');
await page.fill('#password-confirm', 'DifferentPassword!');
await page.click('#register-button');
// Then: エラーメッセージが表示される
await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toContainText('パスワードが一致しません');
// Then: 登録フォームに留まる
await expect(page).toHaveURL('https://example.com/register');
});
test('必須項目が未入力の場合、送信できない', async ({ page }) => {
// Given: 登録フォームにアクセス
await page.goto('https://example.com/register');
// When: メールアドレスを入力せずに送信
await page.fill('#username', 'newuser123');
await page.fill('#password', 'Password123!');
await page.click('#register-button');
// Then: フォームのバリデーションエラーが表示される
await expect(page.locator('#email-error')).toBeVisible();
await expect(page.locator('#email-error')).toContainText('メールアドレスは必須です');
});
});
例3: 検索機能
test.describe('商品検索', () => {
test('キーワードで商品を検索できる', async ({ page }) => {
// Given: トップページにアクセス
await page.goto('https://example.com');
// When: 「ノートPC」で検索
await page.fill('#search-input', 'ノートPC');
await page.click('#search-button');
// Then: 検索結果ページに遷移
await expect(page).toHaveURL(/.*search\?q=ノートPC/);
// Then: 検索結果が表示される
await expect(page.locator('.search-results')).toBeVisible();
await expect(page.locator('.product-card')).not.toHaveCount(0);
// Then: すべての商品タイトルに「ノートPC」が含まれる
const productTitles = await page.locator('.product-title').allTextContents();
productTitles.forEach(title => {
expect(title.toLowerCase()).toContain('ノートpc');
});
});
test('検索結果が0件の場合、メッセージが表示される', async ({ page }) => {
// Given: トップページにアクセス
await page.goto('https://example.com');
// When: 存在しない商品で検索
await page.fill('#search-input', 'xyzabc999');
await page.click('#search-button');
// Then: 検索結果が0件のメッセージが表示される
await expect(page.locator('.no-results-message')).toBeVisible();
await expect(page.locator('.no-results-message')).toContainText('検索結果が見つかりませんでした');
// Then: 商品カードは表示されない
await expect(page.locator('.product-card')).toHaveCount(0);
});
});
[!WARNING] テストの独立性
各テストケースは独立して実行できるようにしましょう。他のテストの実行結果に依存すると、テストが不安定になります。
テストケース作成のチェックリスト
良いテストケースかどうか、以下の項目で確認しましょう:
- 明確な名前:テストが何を検証しているか一目でわかる
- Given-When-Then:3つのフェーズが明確に分かれている
- 1つの責務:1つのテストで1つの動作を検証
- 独立性:他のテストに依存せず、単独で実行可能
- 再現性:何度実行しても同じ結果になる
- 適切な検証:必要十分なアサーションがある
- 読みやすさ:他の開発者が理解しやすい
まとめ
- テストケースはGiven-When-Thenの3つのフェーズで構成
- 前提条件(Given):テストの初期状態を準備
- 実行(When):テスト対象の操作を実行
- 検証(Then):期待する結果を確認
- 1つのテストで1つの動作を検証することが重要
- 各テストは独立して実行できるように設計
- 明確な構造により、読みやすく、メンテナンスしやすいテストを実現
次のチャプターでは、アサーション(検証)の詳細について学びます。