mcpField
T
testing-library

testing-library

github.com/jezwebテスト・品質1.1k203 回インストール3 週間前に更新
リポジトリ

testing-library について

React Testing Library を使う React 開発者向けのテストガイドです。アクセシビリティを優先したクエリやユーザー操作、非同期テストを扱い、MSW による API モック例も含みます。

主な機能

  1. 01アクセシビリティ優先のクエリ選択
  2. 02userEvent による操作の再現
  3. 03非同期 UI 変更のテスト
  4. 04MSW による API モック
  5. 05アクセシビリティ問題の確認
  6. 06フォーム・ダイアログ・リストのテストパターン

活用シーン

  1. 01フォーム送信と入力検証の確認
  2. 02非同期読み込みやダイアログのテスト
  3. 03API の成功・エラーレスポンスのモック
  4. 04コンポーネントのアクセシビリティ確認

インストール

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

$npx skills add https://github.com/jezweb/claude-skills --skill testing-library

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
.claude-plugin/
plugin.json
rules/
testing-library-patterns.md
templates/
msw-handlers.ts
setup.ts

詳細

React Testing Library

ステータス:本番利用可能 最終更新日:2026-02-06 バージョン:16.x User Event:14.x


クイックスタート

# Install with Vitest
pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom

# Or with Jest
pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
セットアップファイル(src/test/setup.ts)
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

// Cleanup after each test
afterEach(() => {
  cleanup();
});
Vitest の設定
// vitest.config.ts
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
});

クエリの優先順位(アクセシビリティを最優先)

アクセシブルで堅牢なテストを作成するには、次の順番でクエリを使用します。

優先順位クエリ用途
1getByRoleボタン、リンク、見出し、入力欄
2getByLabelTextラベル付きのフォーム入力欄
3getByPlaceholderText表示ラベルのない入力欄
4getByTextインタラクティブではないテキストコンテンツ
5getByTestId最終手段のみ
例
import { render, screen } from '@testing-library/react';

// ✅ GOOD - semantic role queries
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('link', { name: /learn more/i });

// ✅ GOOD - label-based queries for forms
screen.getByLabelText(/email address/i);

// ⚠️ OK - when no better option
screen.getByText(/welcome to our app/i);

// ❌ AVOID - not accessible, brittle
screen.getByTestId('submit-button');

クエリのバリエーション

バリエーション戻り値例外用途
getBy要素発生する要素が今存在する
queryBy要素または null発生しない要素が存在しない可能性がある
findByPromise発生する非同期で後から表示される
getAllByElement[]発生する複数の要素
queryAllByElement[]発生しない複数または該当なし
findAllByPromise<Element[]>発生する非同期で複数の要素
使い分け
// Element exists immediately
const button = screen.getByRole('button');

// Check element doesn't exist
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

// Wait for async element to appear
const modal = await screen.findByRole('dialog');

// Multiple elements
const items = screen.getAllByRole('listitem');

User Event(実際に近いインタラクション)

fireEvent ではなく、必ず userEvent を使用してください。実際のユーザーの操作をシミュレートします。

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

describe('Form', () => {
  it('submits form data', async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();

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

    // 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(onSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'secret123',
    });
  });
});
よく使うユーザーイベント
const user = userEvent.setup();

// Clicking
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element); // Select all text

// Typing
await user.type(input, 'hello world');
await user.clear(input);
await user.type(input, '{Enter}'); // Special keys

// Keyboard
await user.keyboard('{Shift>}A{/Shift}'); // Shift+A
await user.tab(); // Tab navigation

// Selection
await user.selectOptions(select, ['option1', 'option2']);

// Hover
await user.hover(element);
await user.unhover(element);

// Clipboard
await user.copy();
await user.paste();

非同期テスト

findBy - 要素が表示されるまで待機
it('shows loading then content', async () => {
  render(<AsyncComponent />);

  // Shows loading initially
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // Wait for content to appear (auto-retries)
  const content = await screen.findByText(/data loaded/i);
  expect(content).toBeInTheDocument();
});
waitFor - 条件が満たされるまで待機
import { waitFor } from '@testing-library/react';

it('updates count after click', async () => {
  const user = userEvent.setup();
  render(<Counter />);

  await user.click(screen.getByRole('button', { name: /increment/i }));

  // Wait for state update
  await waitFor(() => {
    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });
});
waitForElementToBeRemoved
import { waitForElementToBeRemoved } from '@testing-library/react';

it('hides modal after close', async () => {
  const user = userEvent.setup();
  render(<ModalComponent />);

  await user.click(screen.getByRole('button', { name: /close/i }));

  // Wait for modal to disappear
  await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});

MSW 連携(API のモック)

Mock Service Worker を使用して、ネットワークレベルで API 呼び出しをモックします。

pnpm add -D msw
セットアップ(src/test/mocks/handlers.ts)
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/user', () => {
    return HttpResponse.json({
      id: 1,
      name: 'Test User',
      email: '[email protected]',
    });
  }),

  http.post('/api/login', async ({ request }) => {
    const body = await request.json();
    if (body.password === 'correct') {
      return HttpResponse.json({ token: 'abc123' });
    }
    return HttpResponse.json(
      { error: 'Invalid credentials' },
      { status: 401 }
    );
  }),
];
セットアップ(src/test/mocks/server.ts)
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
テストのセットアップ
// src/test/setup.ts
import { server } from './mocks/server';
import { beforeAll, afterEach, afterAll } from 'vitest';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
テストでの使用
import { server } from '../test/mocks/server';
import { http, HttpResponse } from 'msw';

it('handles API error', async () => {
  // Override handler for this test
  server.use(
    http.get('/api/user', () => {
      return HttpResponse.json(
        { error: 'Server error' },
        { status: 500 }
      );
    })
  );

  render(<UserProfile />);

  await screen.findByText(/error loading user/i);
});

アクセシビリティテスト

A11y 違反の確認
pnpm add -D @axe-core/react
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('has no accessibility violations', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
ロールベースのクエリは A11y テストです

getByRole を使用すると、暗黙的にアクセシビリティをテストできます。

// This passes only if button is properly accessible
screen.getByRole('button', { name: /submit/i });

// Fails if:
// - Element isn't a button or role="button"
// - Accessible name doesn't match
// - Element is hidden from accessibility tree

テストパターン

フォーム
it('validates required fields', async () => {
  const user = userEvent.setup();
  render(<ContactForm />);

  // Submit without filling required fields
  await user.click(screen.getByRole('button', { name: /submit/i }));

  // Check for validation errors
  expect(screen.getByText(/email is required/i)).toBeInTheDocument();
  expect(screen.getByText(/message is required/i)).toBeInTheDocument();
});
モーダル/ダイアログ
it('opens and closes modal', async () => {
  const user = userEvent.setup();
  render(<ModalTrigger />);

  // Modal not visible initially
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

  // Open modal
  await user.click(screen.getByRole('button', { name: /open/i }));
  expect(screen.getByRole('dialog')).toBeInTheDocument();

  // Close modal
  await user.click(screen.getByRole('button', { name: /close/i }));
  await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});
リスト
it('renders list items', () => {
  render(<TodoList items={['Buy milk', 'Walk dog']} />);

  const items = screen.getAllByRole('listitem');
  expect(items).toHaveLength(2);
  expect(items[0]).toHaveTextContent('Buy milk');
});

よく使うマッチャー(jest-dom)

// Presence
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEmptyDOMElement();

// State
expect(button).toBeEnabled();
expect(button).toBeDisabled();
expect(checkbox).toBeChecked();
expect(input).toBeRequired();

// Content
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveValue('test');
expect(element).toHaveAttribute('href', '/about');

// Styles
expect(element).toHaveClass('active');
expect(element).toHaveStyle({ color: 'red' });

// Focus
expect(input).toHaveFocus();

デバッグ

screen.debug()
it('debugs rendering', () => {
  render(<MyComponent />);

  // Print entire DOM
  screen.debug();

  // Print specific element
  screen.debug(screen.getByRole('button'));
});
logRoles
import { logRoles } from '@testing-library/react';

it('shows available roles', () => {
  const { container } = render(<MyComponent />);
  logRoles(container);
});

よくある間違い

非同期処理で getBy を使用する
// ❌ WRONG - fails if element appears async
const modal = screen.getByRole('dialog');

// ✅ CORRECT - waits for element
const modal = await screen.findByRole('dialog');
ユーザーイベントを await しない
// ❌ WRONG - race condition
user.click(button);
expect(result).toBeInTheDocument();

// ✅ CORRECT - await the interaction
await user.click(button);
expect(result).toBeInTheDocument();
container.querySelector を使用する
// ❌ WRONG - not accessible, brittle
const button = container.querySelector('.submit-btn');

// ✅ CORRECT - accessible query
const button = screen.getByRole('button', { name: /submit/i });

関連項目

コメント