Communitygithub.com

React Testing

React Testing Library, Vitest/Jest を用いた React コンポーネントテスト、MSW によるネットワークモック、axe を用いたアクセシビリティ検証、およびコンポーネントテストと Playwright/Cypress E2E テストの境界判断を提供します。React コンポーネント、フック、ページのテスト作成・修正時に利用します。

React Testing とは?

このスキルは、React アプリケーションにおけるテスト戦略を包括的にカバーします。核となるのは「実装の詳細ではなく、ユーザーが目にし操作する動作をテストする」という原則です。React Testing Library を中心に、Vitest や Jest といったテストランナー、`userEvent` によるユーザー操作のシミュレーション、MSW によるネットワークレイヤーのモックを扱います。また、`jest-axe` / `vitest-axe` を用いたアクセシビリティチェック、カスタムフックのテスト方法、プロバイダラッパーの共通化、スナップショットテストの適切な使いどころなども解説します。さらに、JSDOM ベースのテストでは不十分なケース(レイアウト、ブラウザ固有 API)について Playwright/Cypress コンポーネントテストや E2E テストに切り替える判断基準も提供します。テスト駆動開発(TDD)のワークフローやカバレッジ目標、アンチパターンの回避方法についても詳述されており、開発者が堅牢でメンテナブルな React テストスイートを構築するための実践的なガイドとなります。

対応✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/react-testing

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

React Testing は何をしますか?

React コンポーネントのテストに関する包括的なパターン集です。振る舞いに焦点を当てたコンポーネントテスト、カスタムフックのテスト、アクセシビリティ検証、ネットワークレベルのモックを扱います。

いつ Activate するか

  • React コンポーネント、カスタムフック、ページのテストを書くとき
  • レガシーな未テストコンポーネントにテストカバレッジを追加するとき
  • Enzyme やクラスコンポーネント時代のパターンから React Testing Library へ移行するとき
  • 新しい React プロジェクトで Vitest または Jest をセットアップするとき
  • テスト内で HTTP リクエストをモックするとき
  • アクセシビリティ違反を検証するとき
  • どのテストを RTL で書くべきか、Playwright コンポーネントテストで書くべきか、あるいは完全な E2E で書くべきかを判断するとき

核となる原則

ユーザーが見て操作するものをテストし、実装の詳細はテストしないこと。

テストは以下を行うべきです:

  • プロダクションと同じプロバイダーでコンポーネントをレンダリングする
  • アクセシブルなクエリ(role, label)と userEvent を使って操作する
  • 表示される出力と観測可能な副作用(コールバックの呼び出し、リクエストの送信)を検証する

テストは以下を行うべきではありません:

  • コンポーネントの状態、子コンポーネントに渡される props、呼び出されたフックを検査する
  • React 自体やフレームワークのフックをモックする
  • レンダリング回数や、ユーザーに影響を与えない DOM 構造を検証する

ライブラリの選択

ランナー使用場面備考
VitestVite, Remix, モダンなセットアップ高速、ネイティブ ESM、Jest 互換 API
JestNext.js, CRA, 既存のリポジトリ多くの React プロジェクトでのデフォルト
Playwright コンポーネントテスト実際のブラウザエンジンが必要な場合JSDOM に不足している機能が必要なときに使用
Cypress コンポーネントテスト実際のブラウザ、かつ Cypress を既に使用している場合Playwright CT の代替

1 つを選んでください。明確なレーン分離がない限り、同じリポジトリ内で RTL + Vitest と Playwright CT を併用しないでください。

クエリ優先度

React Testing Library は 3 つの階層でクエリを提供します。上位から使用してください:

  1. すべてのユーザーがアクセス可能: getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue
  2. セマンティック: getByAltText, getByTitle
  3. テスト ID(最終手段): getByTestId
// ベスト
screen.getByRole("button", { name: /save/i });

// 入力項目には OK
screen.getByLabelText("Email");

// 最終手段
screen.getByTestId("save-btn");

バリエーション:

  • getBy* — 見つからない場合はエラーをスロー
  • queryBy* — null を返す(「存在しないことの表明」に使用)
  • findBy* — 非同期、Promise を返す(非同期処理の後に表示される要素に使用)

userEvent によるユーザー操作

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

test("フォームを送信する", async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<UserForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText("Email"), "[email protected]");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(onSubmit).toHaveBeenCalledWith({ email: "[email protected]" });
});
  • userEvent の呼び出しは常に await すること
  • テストごとに userEvent.setup() を 1 回呼び出し、返された user を使い回すこと
  • userEvent は実際のブラウザの一連の操作をシミュレートします。fireEvent は単一の合成イベントをディスパッチするだけです。userEvent を優先してください

非同期パターン

// 非同期処理の後に現れる要素
await expect(screen.findByText("Loaded")).resolves.toBeInTheDocument();

// 副作用の検証
await waitFor(() => expect(saveSpy).toHaveBeenCalled());

// 消えるべき要素
await waitForElementToBeRemoved(() => screen.queryByText("Loading"));

setTimeout + アサーションは絶対に使用しないでください。不安定になります。上記のマッチャーを使用してください。

MSW によるネットワークモック

Mock Service Worker はネットワーク層でモックを行います。コンポーネント、フック、フェッチライブラリはすべてプロダクションとまったく同じように動作します。

セットアップ

// 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());

onUnhandledRequest: "error" を設定すると、モックされていないリクエストがあった場合にテストが確実に失敗します。沈黙のパスはレッドよりも悪質です。

テストごとの上書き

test("500 エラーをレンダリングする", async () => {
  server.use(
    http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
  );
  render(<UserPage id="1" />);
  await expect(screen.findByText(/something went wrong/i)).resolves.toBeInTheDocument();
});

プロバイダーのラッピング

プロバイダーは 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";

そして、すべてのテストファイルで import { renderWithProviders, screen } from "test-utils" を使用します。

カスタムフックのテスト

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

test("useCounter がインクリメントとデクリメントを行う", () => {
  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 が初期値を受け入れる", () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});

test("useUser がユーザーデータを取得する", async () => {
  // QueryClient はラッパーの外側でテストごとに 1 回インスタンス化し、再レンダリングをまたいでキャッシュ状態を維持できるようにします。
  // ラッパークロージャ内で作成すると、レンダリングのたびにキャッシュがリセットされ、テストが不安定になります。
  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" });
});
  • 状態を変更する呼び出しは act でラップすること
  • フックのパブリック API のみをテストすること
  • コンテキストを使用するフックの場合は wrapper を渡すこと

アクセシビリティ検証

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

test("UserCard にアクセシビリティ違反がないこと", async () => {
  const { container } = render(<UserCard user={mockUser} />);
  await expect(axe(container)).resolves.toHaveNoViolations();
});

すべてのインタラクティブなコンポーネントのコンポーネントテストで axe を実行してください。以下を検出します:

  • フォーム入力のラベル欠落
  • 無効な ARIA の使用
  • 不十分な色のコントラスト(限定的 — JSDOM には実際の CSS エンジンがないため、インラインスタイルのみで機能します。視覚的なコントラストは Playwright の担当です)
  • 画像の alt テキスト欠落
  • 見出し順序の違反

関連リンク: skills/accessibility/SKILL.md より広範なアクセシビリティテストのプレイブックについて。

スナップショットテストを使用すべきでない場合

レンダリング出力のスナップショットは:

  • スタイル変更のたびに壊れる
  • レビュー時に機械的に承認される
  • 実装の詳細(DOM 構造)をテストし、振る舞いをテストしない

許容されるスナップショットの使用例:

  • 純粋なデータシリアライズ関数(formatInvoice(invoice) -> 安定した文字列)
  • 生成された設定ファイル(例:webpack 設定出力)

コンポーネントのビジュアルリグレッションには、Playwright/Cypress のスクリーンショット、または Percy/Chromatic を使用してください。DOM 文字列ではなく、実際のビジュアル差分を確認します。

Playwright / Cypress を使うべき時

JSDOM(Vitest/Jest が使用)では以下ができません:

  • 実際のレイアウトのレンダリング(flexbox、grid、ビューポートクエリ)
  • ネイティブブラウザアニメーション、CSS トランジションの実行
  • スクロール動作、ドラッグ&ドロップ、クリップボードからの貼り付けのテスト
  • iframe、ポップアップ、ダウンロード、クロスオリジンフローの処理
  • 完全な DevTools サポートを備えた制御された環境での実際のネットワークの実行

これらには、Playwright コンポーネントテスト(実際のブラウザでのコンポーネントテスト)または完全な E2E を使用してください。e2e-testing skill を参照してください。

判断基準:

  • フック、プレゼンテーショナルコンポーネント、ロジックを含むフォーム -> RTL
  • レイアウトが重要、または JSDOM にないブラウザ API を使用するコンポーネント -> Playwright CT
  • 複数ページにわたる完全なユーザーフロー -> Playwright/Cypress E2E

カバレッジ目標

レイヤー目標
純粋なユーティリティ>=90%
カスタムフック>=85%
プレゼンテーショナルコンポーネント>=80% — 行数ではなく動作
コンテナコンポーネント>=70% — ゴールデンパス + エラー状態
ページE2E で別途カバー。最低限スモークテスト

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,
    },
  },
}

アンチパターン

  • container.querySelector("...") — アクセシビリティクエリをバイパスし、実際のユーザーが失敗するようなテストを通してしまう
  • レンダリング回数を検証する — 実装の詳細
  • jest.mock("react", ...) — React をモックしてはいけません。代わりにコンポーネントをリファクタリングしてください
  • 子コンポーネントをデフォルトでモックする — 統合ではなく分離をテストすることになります。子コンポーネントに重い副作用がある場合のみモックしてください
  • act() の警告を無視する — これは実際のバグ(アンマウント後の状態更新、非同期ラッピングの欠落)を示しています
  • テスト間で可変状態を共有する — テスト順序が変わると不安定になります
  • it.skip() を削除してもパスするテスト — そのテストは実際に意図したとおりにアサーションを行っていません

TDD ワークフロー

RED     -> 次の要件に対する失敗するテストを書く
GREEN   -> テストを通すための最小限のコンポーネントコードを書く
REFACTOR -> コンポーネントを改善し、テストはグリーンのまま
REPEAT  -> 次の要件へ

新しいコンポーネントの場合:

  1. コンポーネントの props の型とシグネチャを定義する
  2. 最も単純なケースの最初のテストを書く
  3. 正しい理由で失敗することを確認する
  4. テストを通すのに十分なコードだけを実装する
  5. 次のテストケースを追加する
  6. 3 つ目の類似テストがパターンを明らかにしたらリファクタリングする

テストコマンド

# Vitest
vitest                            # ウォッチモード
vitest run                        # 1 回実行
vitest run --coverage             # カバレッジ付き
vitest run path/to/file.test.tsx  # 単一ファイル

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

# CI モード
CI=true vitest run --coverage

関連

例

MSW と userEvent を使用したフォーム送信

test("ユーザーフォームを送信し、成功を表示する", 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"), "[email protected]");
  await user.click(screen.getByRole("button", { name: /save/i }));

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

エラーバウンダリのテスト

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

test("エラーバウンダリがフォールバックをレンダリングする", () => {
  // 予期されるスローに対する React の console.error ノイズを抑制し、その後復元して
  // スパイがテスト間でリークして他の場所で実際のエラーを隠さないようにします。
  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();
  }
});

Suspense バウンダリのテスト

test("ローディング後にコンテンツを表示する", async () => {
  renderWithProviders(
    <Suspense fallback={<div>Loading...</div>}>
      <UserDetail id="1" />
    </Suspense>,
  );

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

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

関連スキル