テストの粒度 — 単体・結合・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, Jest | Testing Library, MSW | Playwright, Cypress |
重要なのは、3つのテストは 代替ではなく補完 の関係だということです。単体テストでロジックを固め、結合テストで繋ぎ目を確認し、E2Eテストでユーザー体験を保証する。この組み合わせが、テスティングピラミッドが示すバランスです。
まとめ
- テストには 単体・結合・E2E の3つの粒度がある
- テスティングピラミッド は「下層ほど多く、上層は厳選」のバランスを示す
- 単体テスト はロジック、結合テスト は接続部分、E2Eテスト はユーザー体験を検証する
- E2Eテストはユーザーに最も近いが、実行コストが高く壊れやすいため、クリティカルパスに絞る
- 3つのテストは代替ではなく補完の関係
次の章では、「結合テスト」の実体をもう少し掘り下げて、何と何を「結合」して確認するのかを具体的に見ていきます。