mcpField
R
react-testing

react-testing

github.com/majesticlabs-devテスト・品質5071 回インストール7 週間前に更新
リポジトリ

react-testing について

react-testing は、React アプリケーションのテスト方法を提供するスキルです。Vitest、React Testing Library、Jest を使ったコンポーネント、フック、統合、モック、アクセシビリティのテストを扱い、実装詳細よりユーザーの操作に注目します。

主な機能

  1. 01コンポーネント、フォーム、カスタムフックのテスト
  2. 02Context や Router を使った統合テスト
  3. 03API とコンポーネント依存のモック
  4. 04アクセシビリティ上の問題を確認
  5. 05ユーザーの操作を重視したテスト実践

活用シーン

  1. 01コンポーネントやフォームのユニットテスト
  2. 02フックと非同期データの動作確認
  3. 03ルーティングと Context の統合テスト
  4. 04CI/CD での回帰テスト実行

インストール

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

$npx skills add https://github.com/majesticlabs-dev/majestic-marketplace --skill react-testing

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

詳細

React のテスト

概要

このスキルでは、Vitest、React Testing Library、Jest を使用して React アプリケーションをテストするための包括的なガイダンスを提供します。ユニットテストや統合テストを作成し、コード品質を確保するときに、これらのパターンを活用してください。

基本理念

  • 実装ではなく振る舞いをテストする:ユーザーに何が見え、何ができるかに焦点を当てます
  • 内部状態のテストを避ける:公開 API とユーザー操作をテストします
  • 自信につながるテストを書く:誤検出ではなく、実際のバグを検出します
  • テストをシンプルに保つ:テストはコードよりも理解しやすいものであるべきです

テストツール

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

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'src/test/']
    }
  }
});
テストのセットアップ
// src/test/setup.ts
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';

expect.extend(matchers);

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

コンポーネントのテスト

基本的なコンポーネントテスト
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import { Button } from './Button';

test('renders button with label', () => {
  render(<Button label="Click me" onClick={vi.fn()} />);
  expect(screen.getByText('Click me')).toBeInTheDocument();
});

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

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

test('is disabled when prop is set', () => {
  render(<Button label="Click me" onClick={vi.fn()} disabled />);
  expect(screen.getByRole('button')).toBeDisabled();
});
フォームのテスト
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

test('submits form with email and password', async () => {
  const handleSubmit = vi.fn();
  render(<LoginForm onSubmit={handleSubmit} />);

  await userEvent.type(screen.getByLabelText('Email'), '[email protected]');
  await userEvent.type(screen.getByLabelText('Password'), 'password123');
  await userEvent.click(screen.getByRole('button', { name: 'Login' }));

  await waitFor(() => {
    expect(handleSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'password123'
    });
  });
});

test('shows validation error for invalid email', async () => {
  render(<LoginForm onSubmit={vi.fn()} />);

  await userEvent.type(screen.getByLabelText('Email'), 'invalid-email');
  await userEvent.click(screen.getByRole('button', { name: 'Login' }));

  expect(await screen.findByText('Invalid email address')).toBeInTheDocument();
});

フックのテスト

カスタムフックのテスト
import { renderHook, act } from '@testing-library/react';
import { expect, test } from 'vitest';
import { useCounter } from './useCounter';

test('increments counter', () => {
  const { result } = renderHook(() => useCounter());

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

test('decrements counter', () => {
  const { result } = renderHook(() => useCounter(10));

  act(() => {
    result.current.decrement();
  });

  expect(result.current.count).toBe(9);
});
依存関係を持つフック
import { renderHook, waitFor } from '@testing-library/react';
import { useApi } from './useApi';

test('fetches data successfully', async () => {
  global.fetch = vi.fn(() =>
    Promise.resolve({
      ok: true,
      json: () => Promise.resolve({ name: 'John' })
    })
  ) as any;

  const { result } = renderHook(() => useApi('/api/user'));

  expect(result.current.loading).toBe(true);

  await waitFor(() => {
    expect(result.current.loading).toBe(false);
  });

  expect(result.current.data).toEqual({ name: 'John' });
  expect(result.current.error).toBeNull();
});

test('handles error', async () => {
  global.fetch = vi.fn(() =>
    Promise.reject(new Error('Network error'))
  );

  const { result } = renderHook(() => useApi('/api/user'));

  await waitFor(() => {
    expect(result.current.error).toBeTruthy();
  });

  expect(result.current.data).toBeUndefined();
});

統合テスト

コンテキストを使用する場合
import { render, screen } from '@testing-library/react';
import { AuthProvider } from './AuthContext';
import { Dashboard } from './Dashboard';

const renderWithAuth = (ui: ReactElement, { user = null } = {}) => {
  return render(
    <AuthProvider value={{ user }}>
      {ui}
    </AuthProvider>
  );
};

test('shows dashboard when authenticated', () => {
  renderWithAuth(<Dashboard />, { user: { name: 'John' } });
  expect(screen.getByText('Welcome, John')).toBeInTheDocument();
});

test('redirects to login when not authenticated', () => {
  renderWithAuth(<Dashboard />);
  expect(screen.queryByText('Welcome')).not.toBeInTheDocument();
});
ルーターを使用する場合
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { App } from './App';

test('navigates to profile page', async () => {
  render(
    <MemoryRouter initialEntries={['/profile']}>
      <App />
    </MemoryRouter>
  );

  expect(screen.getByText('User Profile')).toBeInTheDocument();
});

モック

API のモック
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.json([
        { id: '1', name: 'John' },
        { id: '2', name: 'Jane' }
      ])
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('displays users from API', async () => {
  render(<UserList />);

  expect(await screen.findByText('John')).toBeInTheDocument();
  expect(await screen.findByText('Jane')).toBeInTheDocument();
});
コンポーネントのモック
import { vi } from 'vitest';

vi.mock('./HeavyComponent', () => ({
  HeavyComponent: () => <div>Mocked Component</div>
}));

test('renders page with mocked component', () => {
  render(<Dashboard />);
  expect(screen.getByText('Mocked Component')).toBeInTheDocument();
});

アクセシビリティのテスト

import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('has no accessibility violations', async () => {
  const { container } = render(<LoginForm onSubmit={vi.fn()} />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

ベストプラクティス

  1. セマンティッククエリを使用する:getByTestId よりも getByRole、getByLabelText を使用します
  2. ユーザーの振る舞いをテストする:ユーザーと同じようにクリック、入力、移動を行います
  3. 実装の詳細を避ける:状態や props を直接テストしないでください
  4. 非同期処理には findBy を使用する:要素が表示されるまで自動的に待機します
  5. クリーンアップする:afterEach(cleanup) を使用して DOM をリセットします
  6. 外部依存関係をモックする:API、タイマー、モジュールをモックします
  7. エラー状態をテストする:読み込み中、エラー、空の状態をテストします
  8. テストを絞り込む:可能な場合は、テストごとにアサーションを 1 つにします
  9. 説明的なテスト名を付ける:何が起きるべきかを説明します
  10. CI/CD でテストを実行する:デプロイ前にテストが成功することを確認します

コメント