ウェブエンジニア問題集

テストの粒度 — 単体・結合・E2Eの境界線はどこか

ソフトウェアのテストには、関数ひとつを検証するものから、ブラウザを使ってユーザー操作を丸ごと再現するものまで、さまざまな粒度があります。

この章では、テストの粒度を 単体テスト・結合テスト・E2Eテスト の3段階で整理し、それぞれの役割と使いどころを「テスティングピラミッド」を軸に解説します。

学習者学習者

単体テスト・結合テスト・E2Eって言葉は聞くけど、どこからどこまでがどれなのか境界があいまい…。


テスティングピラミッド

計画を立てている男性のイラスト

テストの粒度を図で表したのが テスティングピラミッド です。下から順に「単体テスト」「結合テスト」「E2Eテスト」が積み上がり、上に行くほどユーザーの実際の利用環境に近づきますが、コストも高くなります。

テスト対象実行速度作成コスト信頼性
E2Eテストシステム全体(ブラウザ → サーバー → DB)遅い高いユーザー体験に最も近い
結合テスト複数のモジュールの連携中程度中程度結合部分のバグを検出
単体テスト関数・クラス単体速い低いロジックのバグを検出

ピラミッドの形が示しているのは「下層ほど多く書き、上層は厳選する」というバランスです。E2Eテストだけですべてをカバーしようとすると、実行時間が膨大になり、テストが壊れやすくなります。


単体テストの領域

単体テストは、関数やクラスといった 最小単位のロジック を検証します。外部依存(DB、API、ファイルシステム)はモックに置き換え、対象のコードだけに集中します。

// 税込み価格を計算する関数
function calcTaxIncluded(price: number, taxRate: number): number {
  return Math.floor(price * (1 + taxRate));
}
 
// 単体テスト
test('税率10%で1000円の税込み価格は1100円', () => {
  expect(calcTaxIncluded(1000, 0.1)).toBe(1100);
});

単体テストが得意なのは「この関数のロジックは正しいか」の検証です。一方で、「コンポーネント同士を組み合わせたときに正しく動くか」や「ユーザーが実際に操作して正しい結果が得られるか」は、単体テストではカバーできません。


結合テストの領域

結合テスト(Integration Test)は、複数のモジュールが連携して正しく動くか を検証します。「結合」の範囲は広く、以下のようなパターンがあります。

結合の範囲
コンポーネント同士親コンポーネントと子コンポーネントを組み合わせてレンダリング
フロントエンド + APIフォーム送信 → APIコール → レスポンス表示
API + データベースAPIエンドポイント → DBへの読み書き
複数サービス認証サービス → メインAPI → 外部API

単体テストとの違いは、モックに置き換えず実際のモジュール同士を繋いで動かす 点です。

// 結合テスト — コンポーネントがAPIのレスポンスを正しく表示するか
test('ユーザー一覧がAPIから取得されて表示される', async () => {
  // APIサーバー(またはMSW)が動いている状態で
  render(<UserList />);
 
  // APIからデータを取得して表示されるのを待つ
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('Bob')).toBeInTheDocument();
  });
});

結合テストは「モジュール間の接続部分にバグがないか」を検出するのが得意です。関数単体では正しく動いていても、組み合わせたときにデータの型が合わなかったり、呼び出し順が間違っていたりする問題を見つけられます。


E2Eテストの領域

E2Eテスト(End-to-End Test)は、システム全体をブラウザを通してテストする手法 です。「端から端まで(End to End)」という名前の通り、フロントエンドからバックエンド、データベースまで、ユーザーの実際の操作シナリオに基づいた検証を行います。

// E2Eテスト — ログインから操作までの一連の流れ
test('ユーザーがログインしてプロフィールを編集できる', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[name="email"]', 'alice@example.com');
  await page.fill('[name="password"]', 'password123');
  await page.click('button[type="submit"]');
 
  // ダッシュボードに遷移するのを確認
  await expect(page).toHaveURL('/dashboard');
 
  // プロフィール編集
  await page.click('text=プロフィール');
  await page.fill('[name="name"]', 'Alice Smith');
  await page.click('text=保存');
 
  // 保存されたことを確認
  await expect(page.getByText('変更を保存しました')).toBeVisible();
});

E2Eテストは実際のブラウザ環境を使うため、Cookieの挙動、画面遷移、JavaScriptの実行、CSSによる表示/非表示 など、単体テストや結合テストでは検証が難しい部分までカバーできます。


E2Eテストのメリットとデメリット

メリット

  • ユーザー操作に最も近い: 実際のブラウザ環境を使うため、Cookie、セッション、画面遷移、レスポンシブ表示など、ユーザーが体験するものと同じ環境でテストできる
  • 結合部分のバグを網羅的に検出: フロント → API → DB の全レイヤーを通すため、レイヤー間の不整合を見つけやすい
  • リグレッション防止: 既存機能が壊れていないことを、ユーザー目線で確認できる

デメリット

  • 実行時間が長い: ブラウザの起動、ページ遷移、ネットワーク通信が発生するため、単体テストに比べて桁違いに遅い
  • 作成コストが高い: テストデータの準備、非同期の待機処理、UIの変更への追従など、書くのも維持するのも手間がかかる
  • 壊れやすい: UIの些細な変更(ボタンのテキスト変更など)でテストが失敗することがある
  • 原因特定が難しい: 失敗したとき、フロント・API・DB のどこが原因かすぐにわからない

3つのテストの使い分け

観点単体テスト結合テストE2Eテスト
対象関数・クラスモジュール間の連携システム全体
速度ミリ秒秒〜分
安定性非常に安定安定壊れやすい
得意なことロジックの正しさ接続部分のバグユーザー体験の保証
典型ツールVitest, JestTesting Library, MSWPlaywright, Cypress

重要なのは、3つのテストは 代替ではなく補完 の関係だということです。単体テストでロジックを固め、結合テストで繋ぎ目を確認し、E2Eテストでユーザー体験を保証する。この組み合わせが、テスティングピラミッドが示すバランスです。


まとめ

  • テストには 単体・結合・E2E の3つの粒度がある
  • テスティングピラミッド は「下層ほど多く、上層は厳選」のバランスを示す
  • 単体テスト はロジック、結合テスト は接続部分、E2Eテスト はユーザー体験を検証する
  • E2Eテストはユーザーに最も近いが、実行コストが高く壊れやすいため、クリティカルパスに絞る
  • 3つのテストは代替ではなく補完の関係

次の章では、「結合テスト」の実体をもう少し掘り下げて、何と何を「結合」して確認するのかを具体的に見ていきます。