React Testing Library入門

intermediate | 50分 で読める | 2024.12.30

公式ドキュメント

このチュートリアルで学ぶこと

  • セットアップ
  • 基本的なテストの書き方
  • クエリの使い分け
  • ユーザーイベント
  • 非同期テスト

Step 1: セットアップ

npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event jest jest-environment-jsdom

Step 2: 基本的なテスト

renderでコンポーネントを描画し、screenでDOM要素を取得、expectで検証するのが基本的な流れです。

// Button.tsx
export function Button({ onClick, children }) {
 return <button onClick={onClick}>{children}</button>;
}

// Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

describe('Button', () => {
 it('renders correctly', () => {
 render(<Button>Click me</Button>);
 expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
 });

 it('calls onClick when clicked', async () => {
 const handleClick = jest.fn();
 render(<Button onClick={handleClick}>Click me</Button>);

 await userEvent.click(screen.getByRole('button'));
 expect(handleClick).toHaveBeenCalledTimes(1);
 });
});

Step 3: クエリの使い分け

ポイント: クエリはアクセシビリティを意識した優先順位で選びましょう。getByRoleが最優先、getByTestIdは最終手段です。

// 優先順位(アクセシビリティ重視)
screen.getByRole('button', { name: 'Submit' }); // 最優先
screen.getByLabelText('Email'); // フォーム要素
screen.getByPlaceholderText('Enter email'); // プレースホルダー
screen.getByText('Hello World'); // テキスト
screen.getByTestId('custom-element'); // 最終手段

// 存在確認
screen.queryByRole('button'); // なければnull
await screen.findByRole('button'); // 非同期で待つ

Step 4: フォームテスト

// LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

it('submits form with correct values', async () => {
 const handleSubmit = jest.fn();
 render(<LoginForm onSubmit={handleSubmit} />);

 await userEvent.type(screen.getByLabelText('Email'), 'test@example.com');
 await userEvent.type(screen.getByLabelText('Password'), 'password123');
 await userEvent.click(screen.getByRole('button', { name: 'Login' }));

 expect(handleSubmit).toHaveBeenCalledWith({
 email: 'test@example.com',
 password: 'password123'
 });
});

Step 5: 非同期テスト

実践メモ: 非同期テストではfindByクエリやwaitForを使いましょう。タイムアウトを適切に設定し、不安定なテストを防ぎます

// UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserList } from './UserList';

it('displays users after loading', async () => {
 render(<UserList />);

 expect(screen.getByText('Loading...')).toBeInTheDocument();

 await waitFor(() => {
 expect(screen.getByText('Alice')).toBeInTheDocument();
 });

 expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});

Step 6: モック

注意: モックしすぎると実際の動作との乖離が生まれます。外部API呼び出しはモックしつつ、コンポーネント内のロジックはなるべくそのままテストしましょう

// API モック
jest.mock('./api', () => ({
 fetchUsers: jest.fn().mockResolvedValue([
 { id: 1, name: 'Alice' }
 ])
}));

// コンポーネントモック
jest.mock('./Header', () => ({
 Header: () => <div data-testid="mock-header">Header</div>
}));

ユーザー視点でテストする

React Testing Libraryは、コンポーネントの内部実装ではなく、ユーザーから見える振る舞いをテストする考え方を重視します。stateの値を直接確認するより、ボタンを押した結果として画面に何が表示されるかを確認します。

テスト対象をroleやlabelで取得すると、アクセシビリティの確認にもつながります。逆にclass名やDOM構造に依存しすぎると、見た目のリファクタリングだけでテストが壊れやすくなります。

確認したい観点

  • ボタンや入力欄をroleやlabelで取得できるか
  • ユーザー操作はuserEventで再現しているか
  • 非同期表示はfindBy系で待っているか
  • 実装詳細ではなく画面の結果を見ているか

まとめ

React Testing Libraryはユーザー視点でテストを書くことを推奨します。アクセシビリティを意識したクエリでテストしましょう。

参考リソース

← 一覧に戻る
PR
PR
PR
PR