ウェブエンジニア問題集

結合テストの基本 — 何と何を「結合」して確認するのか

「結合テスト」という言葉は、現場によって指す範囲がかなり違います。コンポーネント同士の組み合わせを結合テストと呼ぶ現場もあれば、フロントエンドからAPIまで通すテストを結合テストと呼ぶ現場もあります。

この章では、結合テストの「結合」が何を意味するのかを整理し、テスト設計の考え方を固めます。

学習者学習者

そもそも「結合」って何と何をくっつけるの?人によって言ってることが違う気がする…。


結合テストとは何か

結合テストは、複数のモジュールを実際に繋いで、その連携が正しく動くかを検証するテスト です。

単体テストが「部品ひとつの動作確認」だとすれば、結合テストは「部品を組み合わせたときに噛み合うかの確認」です。

単体テストでは各関数が正しい値を返すことを確認しますが、結合テストでは「関数Aの出力が関数Bの入力として正しく渡されるか」「コンポーネントCがそのデータを正しく表示するか」を確認します。


結合テストの範囲

分岐で悩む男性のイラスト

結合テストは「何と何を結合するか」によって、いくつかのレベルに分かれます。

レベル結合する対象
コンポーネント結合UIコンポーネント同士フォーム + バリデーション + エラー表示
フロント+API結合フロントエンドとAPIサーバー画面操作 → API呼び出し → レスポンス表示
API+DB結合APIサーバーとデータベースエンドポイント → クエリ実行 → 結果返却
サービス間結合複数のサービス認証API → メインAPI → 外部サービス

下に行くほど範囲が広がり、E2Eテストに近づきます。「結合テスト」と「E2Eテスト」の境界は連続的で、明確な線引きは現場ごとに違います。


モックの境界線

結合テストの設計で最も重要な判断は「どこまでを本物で動かし、どこからモックに置き換えるか」です。

モックの範囲によるテストの性質

完全モック   ←───────────────────→   完全リアル
  単体テスト    結合テスト           E2Eテスト
アプローチモックする対象メリットデメリット
外部境界だけモック外部API、メール送信本物に近い検証テストデータの準備が大変
APIレイヤーをモックバックエンドAPI全体フロント単独で高速に回せるAPI側の問題は見つからない
DBをモックデータベースCI環境で動かしやすいクエリの問題は見つからない

結合テストの典型的なアプローチは、テスト対象の内部はそのまま動かし、外部のシステム境界だけモックに置き換える ことです。

// 例: フロントエンドの結合テスト
// → APIレスポンスはMSW(Mock Service Worker)でモック
// → コンポーネント同士の連携は本物のまま
 
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
 
const server = setupServer(
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]);
  })
);
 
beforeAll(() => server.listen());
afterAll(() => server.close());

結合テストで見つかるバグ

単体テストでは見つからず、結合テストで初めて見つかるバグの典型例です。

1. インターフェースの不一致

// 関数Aは { userName: string } を返す
function fetchUser() {
  return { userName: 'Alice' };
}
 
// コンポーネントBは { name: string } を期待している
function UserCard({ user }: { user: { name: string } }) {
  return <div>{user.name}</div>; // undefined — キー名が違う
}

それぞれの単体テストは通りますが、組み合わせると表示されません。

2. 非同期のタイミング問題

// APIの呼び出し完了前にレンダリングが走り、データがundefined
function UserProfile() {
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    fetchUser().then(setUser);
  }, []);
 
  return <div>{user.name}</div>; // ← 初回レンダリングでエラー
}

3. 状態管理の競合

複数のコンポーネントが同じグローバル状態を更新するとき、更新順序によって結果が変わる問題は、結合テストでないと検出できません。


結合テストの設計指針

1. ユーザーの行動を起点にする

結合テストのシナリオは、内部実装ではなく「ユーザーが何をして、何が起きるか」で書きます。

// NG — 内部実装に依存
test('fetchUserが呼ばれてstateが更新される', () => { ... });
 
// OK — ユーザー行動を起点にする
test('ユーザー一覧ページにアクセスすると、ユーザー名が表示される', () => { ... });

2. テストの独立性を保つ

各テストが他のテストに依存しないようにします。テストデータは各テストの中でセットアップし、後片付けします。

beforeEach(() => {
  // 各テストの前にモックサーバーをリセット
  server.resetHandlers();
});

3. 適切な待機処理を入れる

結合テストでは非同期処理が発生するため、適切に待つ ことが重要です。固定時間の sleep ではなく、条件ベースの待機を使います。

// NG — 固定時間で待つ(遅いし不安定)
await new Promise(r => setTimeout(r, 3000));
 
// OK — 条件を満たすまで待つ
await waitFor(() => {
  expect(screen.getByText('Alice')).toBeInTheDocument();
});

まとめ

  • 結合テストは 複数のモジュールの連携 を検証する
  • 「結合」の範囲はコンポーネント同士からAPI+DB結合まで幅広い
  • テスト対象の内部は本物、外部境界だけモック が典型的なアプローチ
  • 結合テストはバグ検出のコスパが高い — 多くのバグは「接続部分」で発生する
  • テストシナリオは内部実装ではなく ユーザーの行動を起点 に書く

次の章では、結合テストの具体的なパターンとして、UIコンポーネントの結合テスト をTesting Libraryを使って実践します。