ウェブエンジニア問題集

コンポーネント結合テスト — UIパーツを組み合わせて検証する

フロントエンド開発では、複数のコンポーネントを組み合わせて一つの機能を構成します。フォーム入力、バリデーション、API呼び出し、結果表示 — これらが連携して正しく動くかを確認するのがコンポーネント結合テストです。

この章では、Testing LibraryMSW(Mock Service Worker) を使ったコンポーネント結合テストの実践パターンを見ていきます。

学習者学習者

コンポーネントのテストって、何を「正しい」と確認すればいいの?見た目?それとも動き?


Testing Libraryの考え方

Testing Libraryは「ユーザーがUIをどう使うか」の視点でテストを書くためのライブラリです。内部の状態やpropsではなく、画面に表示されるテキストやアクセシブルなロール(role)を使って要素を取得します。

// NG — 内部実装に依存したセレクタ
const input = container.querySelector('.form-input__email');
 
// OK — ユーザーに見えるラベルで取得
const input = screen.getByLabelText('メールアドレス');
 
// OK — ロールで取得
const button = screen.getByRole('button', { name: '送信' });

基本的な結合テストの構造

コンポーネント結合テストは Arrange → Act → Assert の3段階で構成します。

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
 
test('検索フォームに入力すると結果が表示される', async () => {
  const user = userEvent.setup();
 
  // Arrange — コンポーネントをレンダリング
  render(<SearchPage />);
 
  // Act — ユーザー操作をシミュレート
  const input = screen.getByRole('searchbox');
  await user.type(input, 'React');
  await user.click(screen.getByRole('button', { name: '検索' }));
 
  // Assert — 結果を確認
  await waitFor(() => {
    expect(screen.getByText('React入門')).toBeInTheDocument();
  });
});
PCで作業する女性のイラスト

ユーザー操作のシミュレーション

@testing-library/user-event は、実際のユーザー操作に近いイベントを発生させます。fireEvent よりも推奨されます。

テキスト入力

const user = userEvent.setup();
const input = screen.getByLabelText('名前');
await user.type(input, 'Alice');

クリック

await user.click(screen.getByRole('button', { name: '保存' }));

セレクトボックス

await user.selectOptions(
  screen.getByRole('combobox', { name: '都道府県' }),
  '東京都'
);

フォーム入力 → 送信の一連の流れ

test('ユーザー登録フォームが正しく送信される', async () => {
  const user = userEvent.setup();
  render(<RegistrationForm />);
 
  await user.type(screen.getByLabelText('名前'), 'Alice');
  await user.type(screen.getByLabelText('メールアドレス'), 'alice@example.com');
  await user.type(screen.getByLabelText('パスワード'), 'securePass123');
  await user.click(screen.getByRole('button', { name: '登録' }));
 
  await waitFor(() => {
    expect(screen.getByText('登録が完了しました')).toBeInTheDocument();
  });
});

MSWでAPIをモックする

コンポーネントがAPIを呼び出す場合、実際のサーバーを立てる代わりに MSW(Mock Service Worker) でAPIレスポンスをモックします。MSWはブラウザやNode.jsの環境でネットワークリクエストをインターセプトし、定義したレスポンスを返します。

セットアップ

import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
 
const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' },
    ]);
  }),
 
  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json(
      { id: 3, ...body },
      { status: 201 }
    );
  }),
];
 
const server = setupServer(...handlers);
 
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

エラーレスポンスのテスト

テストごとにハンドラーを上書きして、エラーケースも検証できます。

test('API エラー時にエラーメッセージが表示される', async () => {
  // このテストだけハンドラーを上書き
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json(
        { error: 'Internal Server Error' },
        { status: 500 }
      );
    })
  );
 
  render(<UserList />);
 
  await waitFor(() => {
    expect(screen.getByText('データの取得に失敗しました')).toBeInTheDocument();
  });
});

非同期処理の待機パターン

コンポーネント結合テストでは非同期処理が頻出します。Testing Libraryには3つの待機パターンがあります。

パターン使い方用途
findBy*await screen.findByText('Alice')要素が現れるのを待つ
waitForawait waitFor(() => expect(...))条件が満たされるのを待つ
waitForElementToBeRemovedawait waitForElementToBeRemoved(...)要素が消えるのを待つ
// ローディング → データ表示 の流れを検証
test('ローディング後にデータが表示される', async () => {
  render(<UserList />);
 
  // ローディング中の表示を確認
  expect(screen.getByText('読み込み中...')).toBeInTheDocument();
 
  // ローディングが消えるのを待つ
  await waitForElementToBeRemoved(() => screen.queryByText('読み込み中...'));
 
  // データが表示されていることを確認
  expect(screen.getByText('Alice')).toBeInTheDocument();
});

テストの整理パターン

テストが増えてきたら、describe でグループ化し、共通のセットアップをまとめます。

describe('UserList', () => {
  beforeEach(() => {
    server.resetHandlers();
  });
 
  describe('データ取得成功時', () => {
    test('ユーザー名が一覧表示される', async () => { ... });
    test('メールアドレスが表示される', async () => { ... });
  });
 
  describe('データ取得失敗時', () => {
    test('エラーメッセージが表示される', async () => { ... });
    test('リトライボタンが表示される', async () => { ... });
  });
 
  describe('空データ時', () => {
    test('「ユーザーがいません」と表示される', async () => { ... });
  });
});

まとめ

  • コンポーネント結合テストは 複数のUIパーツの連携 を検証する
  • Testing Libraryは ユーザー視点のセレクタ(ラベル、ロール、テキスト)を使う
  • MSW でAPIレスポンスをモックし、ネットワーク越しの連携を再現する
  • 非同期の待機は findBy / waitFor を使い、setTimeout は避ける
  • テストシナリオは ユーザーの操作フロー を起点に書く

次の章では、フロントエンドからバックエンドAPIまでを通す API結合テスト のパターンを見ていきます。