mcpField
R
react-testing

react-testing

github.com/claude-dev-suiteテスト・品質4641 回インストール3 日前に更新
リポジトリ

react-testing について

Vitest と Testing Library を使った React コンポーネントのテストを扱うリファレンスです。React 開発者向けに、ユーザー操作や非同期更新、モックを含む動作中心のテスト方法をまとめています。

主な機能

  1. 01Vitest と Testing Library の設定
  2. 02アクセシブルな要素検索
  3. 03ユーザー操作のテスト
  4. 04非同期状態と更新のテスト
  5. 05関数とモジュールのモック

活用シーン

  1. 01レンダリングした UI でコンポーネントの動作を検証
  2. 02フォーム操作と送信を確認
  3. 03ローディングや非同期データをテスト
  4. 04API モジュールや関数の戻り値をモック

MCP 対応

React のテストドキュメントを取得する `mcp__documentation__fetch_docs` を案内しています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill react-testing

インストール後、Claude Code で/react-testingを入力して読み込まれたか確認します。

同梱ファイルadvanced.md

詳細

React のテスト

完全なリファレンス:MSW のセットアップ、フックのテスト、コンテキストのテスト、フォーム、アクセシビリティテスト、スナップショット、テストパターンについては、advanced.md を参照してください。

詳細な知識:包括的なドキュメントについては、technology: react、topic: testing を指定して mcp__documentation__fetch_docs を使用してください。

Vitest + Testing Library のセットアップ

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/test/setup.ts',
    css: true,
  },
});

// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

afterEach(() => {
  cleanup();
});

基本的なコンポーネントのテスト

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';

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

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

    await userEvent.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button onClick={() => {}} disabled>Click me</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

クエリメソッド

// Priority order (prefer accessible queries)
// 1. getByRole - accessible to everyone
screen.getByRole('button', { name: /submit/i });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('heading', { level: 1 });

// 2. getByLabelText - for form fields
screen.getByLabelText(/email address/i);

// 3. getByPlaceholderText
screen.getByPlaceholderText(/enter your email/i);

// 4. getByText - for non-interactive elements
screen.getByText(/welcome to our app/i);

// 5. getByDisplayValue - for input values
screen.getByDisplayValue(/[email protected]/i);

// 6. getByAltText - for images
screen.getByAltText(/user avatar/i);

// 7. getByTitle
screen.getByTitle(/close/i);

// 8. getByTestId - last resort
screen.getByTestId('custom-element');

// Query variants
screen.getByRole('button');     // Throws if not found
screen.queryByRole('button');   // Returns null if not found
screen.findByRole('button');    // Returns Promise, waits for element
screen.getAllByRole('button');  // Returns array, throws if none
screen.queryAllByRole('button'); // Returns array (possibly empty)
screen.findAllByRole('button'); // Returns Promise of array

ユーザーイベント

import userEvent from '@testing-library/user-event';

describe('Form', () => {
  it('submits with user input', async () => {
    const user = userEvent.setup();
    const handleSubmit = vi.fn();

    render(<LoginForm onSubmit={handleSubmit} />);

    // Type in inputs
    await user.type(screen.getByLabelText(/email/i), '[email protected]');
    await user.type(screen.getByLabelText(/password/i), 'secret123');

    // Click submit
    await user.click(screen.getByRole('button', { name: /sign in/i }));

    expect(handleSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'secret123',
    });
  });

  it('handles keyboard navigation', async () => {
    const user = userEvent.setup();
    render(<Form />);

    // Tab through inputs
    await user.tab();
    expect(screen.getByLabelText(/email/i)).toHaveFocus();

    await user.tab();
    expect(screen.getByLabelText(/password/i)).toHaveFocus();

    // Type and submit with Enter
    await user.type(screen.getByLabelText(/password/i), 'secret{Enter}');
  });

  it('handles select and checkbox', async () => {
    const user = userEvent.setup();
    render(<SettingsForm />);

    // Select option
    await user.selectOptions(screen.getByRole('combobox'), ['dark']);

    // Toggle checkbox
    await user.click(screen.getByRole('checkbox', { name: /notifications/i }));

    expect(screen.getByRole('checkbox')).toBeChecked();
  });
});

非同期テスト

describe('UserProfile', () => {
  it('shows loading then user data', async () => {
    render(<UserProfile userId="123" />);

    // Initially shows loading
    expect(screen.getByText(/loading/i)).toBeInTheDocument();

    // Wait for data to load
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });

    // Loading is gone
    expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
  });
});

// Using findBy (combines getBy + waitFor)
it('loads and displays items', async () => {
  render(<ItemList />);

  // findBy waits for element
  const items = await screen.findAllByRole('listitem');
  expect(items).toHaveLength(3);
});

// waitForElementToBeRemoved
it('removes loading indicator', async () => {
  render(<DataLoader />);

  await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));

  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

モック

モック関数
import { vi } from 'vitest';

const mockFn = vi.fn();
mockFn.mockReturnValue('default');
mockFn.mockReturnValueOnce('first call');
mockFn.mockImplementation((x) => x * 2);
mockFn.mockResolvedValue({ data: [] });
mockFn.mockRejectedValue(new Error('Failed'));

// Assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('last arg');
モジュールのモック
// Mock a module
vi.mock('@/lib/api', () => ({
  fetchUsers: vi.fn(() => Promise.resolve([{ id: 1, name: 'John' }])),
}));

// Import after mocking
import { fetchUsers } from '@/lib/api';

it('fetches users', async () => {
  render(<UserList />);

  expect(fetchUsers).toHaveBeenCalled();
  await screen.findByText('John');
});

よくある落とし穴

問題原因解決策
テストで要素が見つからない要素が非同期でレンダリングされるfindBy または waitFor を使用する
状態が更新されないact() が不足しているuserEvent を使用する(act を処理します)
テスト同士が影響し合う状態が共有されているafterEach でクリーンアップする
不安定なテスト競合状態適切な非同期パターンを使用する

ベストプラクティス

  • 実装ではなく動作をテストする
  • アクセシブルなクエリ(getByRole)を使用する
  • fireEvent より userEvent を使用する
  • エラー状態をテストする
  • API のモックには MSW を使用する
  • 実装の詳細はテストしない
  • サードパーティライブラリはテストしない
  • スナップショットを使いすぎない

このスキルを使用しない場合

  • エンドツーエンドテスト - 完全な E2E フローには Playwright スキルを使用する
  • バックエンドのテスト - フレームワーク固有のテストスキルを使用する
  • パフォーマンステスト - 専用のパフォーマンステストツールを使用する
  • ビジュアルリグレッションテスト - Percy や Chromatic などのツールを使用する

アンチパターン

アンチパターン問題解決策
実装の詳細をテストするテストが壊れやすいユーザーに見える動作をテストする
最初に getByTestId を使うアクセシビリティをテストできないgetByRole、getByLabelText を優先する
userEvent の代わりに fireEvent を使う実際のユーザー操作をシミュレートできない現実的なテストには userEvent を使用する
非同期更新を待たないテストが不安定になるfindBy または waitFor を使用する
すべてをスナップショットテストする保守が難しい安定した UI に限り、控えめに使用する
サードパーティライブラリをテストする労力が無駄になるライブラリのテストを信頼し、連携部分をテストする
エラー状態をテストしないエッジケースを見逃す読み込み中、エラー、空の状態をテストする
シャローレンダリング統合上の問題を見逃す完全なレンダリングを使用する

問題の簡易解決

問題考えられる原因修正方法
要素が見つからないクエリのタイミング、誤ったクエリ非同期処理には findBy を使用するか、クエリを確認する
テストがタイムアウトする非同期処理が完了しないwaitFor のタイムアウトを確認し、非同期コードを修正する
Act の警告act の外で状態が更新されているuserEvent を使用するか、act() で囲む
不安定なテスト競合状態、タイミング適切な非同期クエリ(findBy、waitFor)を使用する
フックをテストできない実装をテストしているコンポーネントに切り出すか、renderHook を使用する
モックが機能しないインポート後にモックしているコンポーネントをインポートする前にモックする
テスト同士が影響し合う状態が共有されているafterEach でクリーンアップする

リファレンスドキュメント

コメント