本文へ移動
cccskills
無料GitHub で公開日本語紹介

react-testing

Reactの画面部品やフックを、利用者の操作と表示結果に沿ってテストします。通信の模擬やアクセシビリティ確認、実ブラウザーで検証する範囲の判断も支援します。

原文原文の説明を見る

React component testing with React Testing Library, Vitest/Jest, MSW for network mocking, accessibility assertions with axe, and the decision boundary between component tests and Playwright/Cypress end-to-end runs. Use when writing or fixing tests for React components, hooks, or pages.

インストール方法を見る

こんなときに便利

  • Reactのフォームや部品のテスト追加
  • 独自フックと非同期更新の検証
  • APIの成功・失敗応答を模擬したいとき
  • アクセシビリティと検証範囲の確認

日本語での紹介

できること

Reactの画面部品、独自のフック、ページのテストを、利用者が行う操作と目にする結果を中心に作成・修正します。React Testing Libraryで入力やクリックを再現し、読み込み完了やエラー表示を検証します。MSWで通信への応答を模擬し、axeでラベルの不足などのアクセシビリティ上の問題も確認します。

こんなときに便利

テストがない既存部品に検証を追加するときや、非同期処理のテストが不安定なときに向いています。Enzymeからの移行、VitestまたはJestの導入にも使えます。部品単位のテストと、複数ページにまたがる一連の操作を確認するテストの分担も整理します。

使い方の例

「このフォームの入力、送信、成功表示をテストして」「APIが失敗した場合の表示をMSWで検証して」「独自フックのテストを追加し、実ブラウザーで確認すべき項目も整理して」と依頼します。内部状態や描画回数より、画面上の結果や送信されたリクエストを確認する方針です。

注意点

テスト環境にはVitestまたはJestなどを使います。これらで使うJSDOMはブラウザーを模した環境で、実際のレイアウトやスクロールなどの検証には限界があります。必要な場合はPlaywrightやCypressを使い、見た目のコントラストも実ブラウザー側で確認します。

この紹介文は、公開されている SKILL.md をもとに AI(Claude Haiku)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(1)

  • SKILL.md12.9 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

React Testing

Comprehensive React testing patterns for behavior-focused component tests, custom hook tests, accessibility assertions, and network-level mocking.

When to Activate

  • Writing tests for React components, custom hooks, or pages
  • Adding test coverage to legacy untested components
  • Migrating from Enzyme or class-component-era patterns to React Testing Library
  • Setting up Vitest or Jest for a new React project
  • Mocking HTTP requests in tests
  • Asserting accessibility violations
  • Deciding which tests belong in RTL vs Playwright Component Testing vs full E2E

Core Principle

Test what the user sees and does, not implementation details.

A test should:

  • Render the component with the same providers it has in production
  • Interact with it via accessible queries (role, label) and userEvent
  • Assert visible output and observable side effects (callback fired, request sent)

A test should NOT:

  • Inspect component state, props passed to children, or which hooks were called
  • Mock React itself or framework hooks
  • Assert on the number of renders or DOM structure beyond what affects users

Library Choice

RunnerWhenNote
VitestVite, Remix, modern setupsFaster, native ESM, Jest-compatible API
JestNext.js, CRA, established reposDefault for many React projects
Playwright Component TestingReal browser engine neededUse when JSDOM lacks the required feature
Cypress Component TestingReal browser, Cypress already in useAlternative to Playwright CT

Pick one. Do not run RTL + Vitest AND Playwright CT in the same repo unless you have a clear lane separation.

Query Priority

React Testing Library exposes queries in three tiers — use top-down:

  1. Accessible to everyone: getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue
  2. Semantic: getByAltText, getByTitle
  3. Test IDs (escape hatch): getByTestId
// Best
screen.getByRole("button", { name: /save/i });

// OK for inputs
screen.getByLabelText("Email");

// Last resort
screen.getByTestId("save-btn");

Variants:

  • getBy* — throws if no match
  • queryBy* — returns null (use for "assert absence")
  • findBy* — async, returns a Promise (use for elements that appear after async work)

User Interaction with userEvent

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

test("submits the form", async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<UserForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText("Email"), "user@example.com");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(onSubmit).toHaveBeenCalledWith({ email: "user@example.com" });
});
  • Always await userEvent calls
  • Call userEvent.setup() once per test, reuse the returned user
  • userEvent simulates a real browser sequence; fireEvent dispatches a single synthetic event — prefer userEvent

Async Patterns

// Element that appears after async work
expect(await screen.findByText("Loaded")).toBeInTheDocument();

// Side effect assertion
await waitFor(() => expect(saveSpy).toHaveBeenCalled());

// Element that should disappear
await waitForElementToBeRemoved(() => screen.queryByText("Loading"));

Never setTimeout + assertion — flaky. Use the matchers above.

Network Mocking with MSW

Mock Service Worker mocks at the network layer. The component, hooks, and fetch library all behave exactly as in production.

Setup

// test/setup.ts
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users/:id", ({ params }) =>
    HttpResponse.json({ id: params.id, name: "Alice" }),
  ),
  http.post("/api/users", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: "new-id", ...body }, { status: 201 });
  }),
];

export const server = setupServer(...handlers);

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Configure onUnhandledRequest: "error" so any unmocked request fails the test loudly — silent passes are worse than red.

Per-test override

test("renders error on 500", async () => {
  server.use(
    http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
  );
  render(<UserPage id="1" />);
  expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});

Provider Wrapping

Wrap providers once in a test-utils.tsx:

// test-utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

export function renderWithProviders(
  ui: React.ReactElement,
  options?: RenderOptions,
) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  return render(
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={lightTheme}>
        <MemoryRouter>{ui}</MemoryRouter>
      </ThemeProvider>
    </QueryClientProvider>,
    options,
  );
}

export * from "@testing-library/react";

Then import { renderWithProviders, screen } from "test-utils" in every test file.

Custom Hook Testing

import { renderHook, act } from "@testing-library/react";

test("useCounter increments and decrements", () => {
  const { result } = renderHook(() => useCounter(0));

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

  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);
});

test("useCounter accepts initial value", () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});

test("useUser fetches user data", async () => {
  // Instantiate QueryClient ONCE per test outside the wrapper so it survives re-renders.
  // Creating it inside the wrapper closure resets cache state on every render, producing flaky tests.
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  const { result } = renderHook(() => useUser("1"), { wrapper });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toEqual({ id: "1", name: "Alice" });
});
  • Wrap state-changing calls in act
  • Test through the hook's public API only
  • For hooks that use context, pass a wrapper

Accessibility Assertions

import { axe, toHaveNoViolations } from "jest-axe"; // or vitest-axe
expect.extend(toHaveNoViolations);

test("UserCard has no a11y violations", async () => {
  const { container } = render(<UserCard user={mockUser} />);
  expect(await axe(container)).toHaveNoViolations();
});

Run axe in component tests for every interactive component. Catches:

  • Missing labels on form inputs
  • Invalid ARIA usage
  • Poor color contrast (limited — JSDOM has no real CSS engine, so this works for inline styles only; visual contrast belongs in Playwright)
  • Missing alt text on images
  • Heading order violations

Cross-link: skills/accessibility/SKILL.md for the broader a11y testing playbook.

When NOT to Use Snapshot Tests

Snapshots of rendered output:

  • Break on every styling change
  • Get rubber-stamped during review
  • Test implementation detail (DOM structure), not behavior

Acceptable snapshot uses:

  • Pure data serialization functions (formatInvoice(invoice) -> stable string)
  • Generated config files (e.g., webpack config output)

For visual regression on components, use Playwright/Cypress screenshots or Percy/Chromatic — actual visual diffs, not DOM strings.

When to Reach for Playwright / Cypress

JSDOM (used by Vitest/Jest) cannot:

  • Render real layout (flexbox, grid, viewport queries)
  • Run native browser animation, CSS transitions
  • Test scrolling behavior, drag-and-drop, paste from clipboard
  • Handle iframes, popups, downloads, cross-origin flows
  • Run real network in a controlled environment with full DevTools support

For any of those, use Playwright Component Testing (component test in real browser) or full E2E. See e2e-testing skill.

Decision boundary:

  • A hook, a presentational component, a form with logic -> RTL
  • A component whose layout matters or that uses browser APIs not in JSDOM -> Playwright CT
  • A full user flow across multiple pages -> Playwright/Cypress E2E

Coverage Targets

LayerTarget
Pure utilities>=90%
Custom hooks>=85%
Presentational components>=80% — behavior, not lines
Container components>=70% — golden paths + error states
PagesE2E covered separately; smoke test minimum

Configure via vitest.config.ts / jest.config.js:

// vitest.config.ts
test: {
  coverage: {
    provider: "v8",
    reporter: ["text", "html", "lcov"],
    thresholds: {
      lines: 80,
      functions: 80,
      branches: 70,
      statements: 80,
    },
  },
}

Anti-Patterns

  • container.querySelector("...") — bypasses accessibility queries, lets tests pass when real users would fail
  • Asserting on number of renders — implementation detail
  • jest.mock("react", ...) — never mock React. Refactor the component instead
  • Mocking child components by default — tests the integration, not isolation. Mock only when the child has heavy side effects
  • Ignoring act() warnings — they signal real bugs (state update after unmount, missing async wrapping)
  • Sharing mutable state across tests — flakes when test order changes
  • Tests that pass with it.skip() removed — your test does not actually assert what you think

TDD Workflow

RED     -> Write failing test for the next requirement
GREEN   -> Write minimal component code to pass
REFACTOR -> Improve the component, tests stay green
REPEAT  -> Next requirement

For new components:

  1. Define the component's prop type and signature
  2. Write the first test for the simplest case
  3. Verify it fails for the right reason
  4. Implement just enough to pass
  5. Add the next test case
  6. Refactor when the third similar test reveals a pattern

Test Commands

# Vitest
vitest                            # watch
vitest run                        # one-shot
vitest run --coverage             # with coverage
vitest run path/to/file.test.tsx  # single file

# Jest
jest --watch
jest --coverage
jest path/to/file.test.tsx

# CI mode
CI=true vitest run --coverage

Related

Examples

Form submission with MSW and userEvent

test("submits user form and shows success", async () => {
  server.use(
    http.post("/api/users", () =>
      HttpResponse.json({ id: "1", name: "Alice" }, { status: 201 }),
    ),
  );

  const user = userEvent.setup();
  renderWithProviders(<UserForm />);

  await user.type(screen.getByLabelText("Name"), "Alice");
  await user.type(screen.getByLabelText("Email"), "alice@example.com");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(await screen.findByText(/saved successfully/i)).toBeInTheDocument();
});

Testing an error boundary

function Broken() {
  throw new Error("boom");
}

test("error boundary renders fallback", () => {
  // Suppress React's console.error noise for the expected throw, then restore so
  // the spy does not leak across tests and hide real errors elsewhere.
  const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  try {
    render(
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        <Broken />
      </ErrorBoundary>,
    );

    expect(screen.getByText("Something went wrong")).toBeInTheDocument();
  } finally {
    errorSpy.mockRestore();
  }
});

Testing a Suspense boundary

test("shows loading then content", async () => {
  renderWithProviders(
    <Suspense fallback={<div>Loading...</div>}>
      <UserDetail id="1" />
    </Suspense>,
  );

  expect(screen.getByText("Loading...")).toBeInTheDocument();
  expect(await screen.findByText("Alice")).toBeInTheDocument();
});

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

accessibility

無料日本語概要

Web・iOS・Androidの画面を、読み上げやキーボード操作に対応させ、ラベル、配色、操作対象の大きさなどをWCAG 2.2に沿って設計・点検するスキル。

  • アイコンボタンの説明を付けたいとき
  • キーボード操作とモーダルの点検
  • コントラストや操作対象の大きさの確認
affaan-m/ECC27.8万2026年10月12日 更新

agent-architecture-audit

無料日本語概要

AIエージェントの不調を、指示・記憶・ツール実行・画面表示など12の層から調べるスキル。コードやログを根拠に原因を整理し、重要度順の指摘と修正案をまとめます。

  • アプリ内だけで起きる不調を調べたいとき
  • 過去の会話が混ざる原因を調査
  • ツールの未実行や実行の誤報を確認
affaan-m/ECC27.8万2026年10月12日 更新

agent-eval

無料日本語概要

実際の開発課題で複数のコーディングエージェントを比較するスキル。成功率、取得可能なAPI費用、所要時間、繰り返し実行の安定性を測り、選定や更新後の評価に使えます。

  • 実際の開発課題でエージェントを比較
  • 新しいツールやモデルの導入前評価
  • エージェント更新後の性能確認
affaan-m/ECC27.8万2026年10月12日 更新

agent-harness-construction

無料日本語概要

AIエージェントが使うツールの種類や入出力、エラーからの復帰手順を設計・見直します。文脈の情報量も整理し、作業完了率や再試行回数で改善を評価します。

  • エージェントのツールや入力形式の設計
  • ツールの結果と次の行動を明確にしたいとき
  • 安全な再試行と停止条件を定めたいとき
affaan-m/ECC27.8万2026年10月12日 更新

agent-introspection-debugging

無料日本語概要

AIエージェントの失敗や同じ操作の繰り返しを記録し、原因の切り分け、小さな復旧操作、結果の報告まで進める手順を示して、根拠のある再試行につなげるスキル。

  • 同じツール操作を繰り返す原因の調査
  • 会話の肥大化による品質低下の調査
  • ファイルパスや環境の食い違いの確認
affaan-m/ECC27.8万2026年10月12日 更新

agent-introspection-debugging

無料日本語概要

AIエージェントが失敗や同じ操作を繰り返す原因を、エラーと実行状況から整理します。小さな復旧操作を試し、結果と根拠を引き継げる報告にまとめるスキルです。

  • エージェントの連続失敗を診断したいとき
  • 同じツール操作のループ調査
  • 情報の蓄積による出力劣化の点検
affaan-m/ECC27.8万2026年10月5日 更新

affaan-m のスキルをすべて見る

このスキルの問題を報告する