自動テストチュートリアル

Playwright、Seleniumなどの自動テストツールを学ぼう

テストケースの書き方

テストケースの書き方

良いテストケースを書くことは、効果的なテスト戦略の基盤です。このチャプターでは、テストケースの構造、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つのテストに詰め込まない
  2. 明確な操作:何をテストしているのか一目でわかるように
  3. 適切な待機:操作後の状態変化を適切に待つ
// ❌ 悪い例:複数の操作を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つの動作を検証することが重要
  • 各テストは独立して実行できるように設計
  • 明確な構造により、読みやすく、メンテナンスしやすいテストを実現

次のチャプターでは、アサーション(検証)の詳細について学びます。