結合テストの基本 — 何と何を「結合」して確認するのか
「結合テスト」という言葉は、現場によって指す範囲がかなり違います。コンポーネント同士の組み合わせを結合テストと呼ぶ現場もあれば、フロントエンドから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を使って実践します。