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

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

テストの種類

テストの種類

ソフトウェアテストにはさまざまな種類があります。このチャプターでは、単体テスト、結合テスト、E2Eテストの違いと、それぞれの目的について学びます。

テストレベルの分類

テストは、テストする範囲によって3つのレベルに分類されます。

テストピラミッド

        /\
       /  \       E2Eテスト(End-to-End)
      /____\      - 少数
     /      \     - 遅い
    /        \    - 高コスト
   /  結合    \
  /____________\  結合テスト(Integration)
 /              \ - 中程度の数
/                \- 中程度の速度
/     単体        \
/__________________\ 単体テスト(Unit)
                    - 多数
                    - 高速
                    - 低コスト

[!INFO] テストピラミッドの原則

テストピラミッドは、理想的なテストの配分を表しています。基盤となる単体テストを多く、上位の統合テストやE2Eテストは適度な数にすることで、効率的かつ効果的なテスト戦略を構築できます。

単体テスト(Unit Test)

概要

単体テストは、個々の関数やクラスなどの小さな単位をテストします。

項目 詳細
テスト範囲 1つの関数、1つのクラス
実行速度 非常に高速(ミリ秒単位)
テスト数 最も多い(数百〜数千)
依存関係 外部依存を排除(モック使用)

単体テストの例

// テスト対象の関数
function calculateTotalPrice(price: number, quantity: number, taxRate: number): number {
  return price * quantity * (1 + taxRate);
}

// 単体テスト
import { test, expect } from '@playwright/test';

test.describe('calculateTotalPrice', () => {
  test('価格、数量、税率から合計金額を計算する', () => {
    const result = calculateTotalPrice(1000, 2, 0.1);
    expect(result).toBe(2200); // 1000 * 2 * 1.1 = 2200
  });

  test('数量が0の場合、合計金額は0になる', () => {
    const result = calculateTotalPrice(1000, 0, 0.1);
    expect(result).toBe(0);
  });

  test('税率が0の場合、税込み価格は元の価格と同じ', () => {
    const result = calculateTotalPrice(1000, 2, 0);
    expect(result).toBe(2000);
  });
});

単体テストの目的

  • ロジックの正確性を確認:計算、変換、判定などのロジックが正しいか
  • エッジケースの検証:境界値や特殊な入力での動作確認
  • リファクタリングの安全性:コード変更時の影響を即座に検知

結合テスト(Integration Test)

概要

結合テストは、複数のモジュールやコンポーネントの連携をテストします。

項目 詳細
テスト範囲 複数のモジュール、APIとの連携
実行速度 中程度(秒単位)
テスト数 中程度(数十〜数百)
依存関係 実際の依存関係を使用

結合テストの例

// APIとデータベースの連携テスト
import { test, expect } from '@playwright/test';

test.describe('商品API統合テスト', () => {
  test('商品を登録してデータベースから取得できる', async ({ request }) => {
    // 1. 商品を登録(POST)
    const createResponse = await request.post('/api/products', {
      data: {
        name: 'ノートPC',
        price: 100000,
        stock: 10
      }
    });
    expect(createResponse.ok()).toBeTruthy();
    const createdProduct = await createResponse.json();

    // 2. 登録した商品を取得(GET)
    const getResponse = await request.get(`/api/products/${createdProduct.id}`);
    expect(getResponse.ok()).toBeTruthy();
    const product = await getResponse.json();

    // 3. データが正しく保存されているか確認
    expect(product.name).toBe('ノートPC');
    expect(product.price).toBe(100000);
    expect(product.stock).toBe(10);
  });

  test('在庫を更新してデータベースに反映される', async ({ request }) => {
    // 商品作成
    const createResponse = await request.post('/api/products', {
      data: { name: 'マウス', price: 3000, stock: 50 }
    });
    const product = await createResponse.json();

    // 在庫更新
    const updateResponse = await request.patch(`/api/products/${product.id}`, {
      data: { stock: 45 }
    });
    expect(updateResponse.ok()).toBeTruthy();

    // 更新後のデータを確認
    const getResponse = await request.get(`/api/products/${product.id}`);
    const updatedProduct = await getResponse.json();
    expect(updatedProduct.stock).toBe(45);
  });
});

結合テストの目的

  • モジュール間の連携確認:複数のコンポーネントが正しく連携するか
  • API契約の検証:APIの仕様通りにデータのやり取りができるか
  • データフローの確認:データが正しく流れ、変換されるか

E2Eテスト(End-to-End Test)

概要

E2Eテストは、ユーザーの視点で、システム全体をテストします。

項目 詳細
テスト範囲 システム全体(UI、API、DB、外部サービス)
実行速度 遅い(数秒〜数十秒/テスト)
テスト数 少数(数十程度)
依存関係 すべての実際の依存関係を使用

E2Eテストの例

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

test.describe('ECサイト購入フロー', () => {
  test('商品を検索してカートに追加し、購入完了まで', async ({ page }) => {
    // 1. ログイン
    await page.goto('https://example.com/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password123');
    await page.click('button[type="submit"]');
    await expect(page.locator('#user-menu')).toBeVisible();

    // 2. 商品検索
    await page.fill('#search-input', 'ノートPC');
    await page.click('#search-button');
    await expect(page.locator('.search-results')).toBeVisible();

    // 3. 商品詳細ページへ移動
    await page.click('.product-card:first-child');
    await expect(page.locator('#product-title')).toContainText('ノートPC');

    // 4. カートに追加
    await page.click('#add-to-cart');
    await expect(page.locator('#cart-count')).toContainText('1');

    // 5. カート確認
    await page.click('#cart-icon');
    await expect(page.locator('.cart-item')).toHaveCount(1);

    // 6. 購入手続き
    await page.click('#checkout-button');
    await page.fill('#shipping-address', '東京都渋谷区...');
    await page.fill('#card-number', '4111111111111111');
    await page.click('#purchase-button');

    // 7. 購入完了確認
    await expect(page.locator('#order-success')).toBeVisible();
    await expect(page.locator('#order-number')).toBeVisible();
  });
});

E2Eテストの目的

  • ユーザー体験の検証:実際のユーザー操作フローが正常に動作するか
  • システム全体の動作確認:すべてのコンポーネントが連携して動作するか
  • 重要なビジネスシナリオの保護:売上に直結する機能が壊れていないか

[!WARNING] E2Eテストの注意点

E2Eテストは実行時間が長く、環境の影響を受けやすいため、すべてのケースをE2Eで書くのは非効率です。重要なユーザーフローに絞って実装しましょう。

各テストレベルの比較

項目 単体テスト 結合テスト E2Eテスト
テスト範囲 関数、クラス モジュール間連携 システム全体
実行速度 非常に高速 中程度 遅い
テスト数 多い 中程度 少ない
失敗時の原因特定 容易 中程度 困難
メンテナンスコスト 低い 中程度 高い
環境依存 なし あり 高い
実装コスト 低い 中程度 高い

テスト戦略の実例

実際のプロジェクトでは、これらのテストをバランスよく組み合わせます。

ECサイトの例

// 【単体テスト】価格計算ロジック(高速・多数)
test('消費税計算', () => {
  expect(calculateTax(1000, 0.1)).toBe(100);
});

// 【結合テスト】カートAPI(中速・中程度)
test('カートに商品を追加するAPI', async ({ request }) => {
  const response = await request.post('/api/cart/add', {
    data: { productId: 123, quantity: 2 }
  });
  expect(response.ok()).toBeTruthy();
});

// 【E2Eテスト】購入フロー全体(低速・少数)
test('商品購入フロー', async ({ page }) => {
  // ログイン → 検索 → カート追加 → 購入完了
  // ※重要なビジネスフローのみ
});

推奨される配分

プロジェクト規模別のテスト配分例:

小規模プロジェクト:
- 単体テスト:30件
- 結合テスト:10件
- E2Eテスト:5件

中規模プロジェクト:
- 単体テスト:200件
- 結合テスト:50件
- E2Eテスト:20件

大規模プロジェクト:
- 単体テスト:1000件
- 結合テスト:200件
- E2Eテスト:50件

[!TIP] テストの優先順位

  1. まず重要なE2Eテストを書いて、主要フローを保護
  2. 次に複雑なロジックの単体テストを追加
  3. 最後にモジュール間の連携を結合テストで確認

この順序で進めると、効率的にカバレッジを向上できます。

まとめ

  • 単体テスト:関数やクラス単位の小さなテスト。高速で多数実装
  • 結合テスト:モジュール間の連携テスト。中程度の速度と数
  • E2Eテスト:システム全体のテスト。低速だが重要なフローを保護
  • テストピラミッドに従い、バランスの取れたテスト戦略を立てる
  • それぞれのテストレベルには明確な目的があり、適切に使い分けることが重要

次のチャプターでは、テストケースの書き方について学びます。