Communitygithub.com

React Testing

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

React Testing とは?

このスキルは、Reactアプリケーションにおける包括的なテストパターンを提供します。動作重視のコンポーネントテスト、カスタムフックのテスト、アクセシビリティアサーション、ネットワークレベルのモックといった領域をカバーしており、開発者が「ユーザーが見て操作するもの」をテストし、実装の詳細をテストしないという中核的な原則に従うことを支援します。 このスキルの主な有用性は、テストランナー(Vitest/Jest/Playwright/Cypress)の選択基準、アクセシブルなクエリの優先順位、`userEvent`によるユーザー操作のシミュレーション、MSWを用いたHTTPリクエストのモック、axeによるアクセシビリティ違反の検出など、実践的なパターンを網羅している点にあります。また、スナップショットテストの落とし穴やアンチパターンについても警告し、TDDワークフローを推奨します。 対応プラットフォームは、主にReact Testing LibraryとVitest/Jestですが、JSDOMでは不十分な場合にPlaywrightまたはCypressのコンポーネントテストへ移行する判断基準も提供します。動作の仕組みとしては、テスト内で実際のプロバイダ(QueryClient、Routerなど)でコンポーネントをラップし、`userEvent`で操作して、スクリーン上の出力やコールバックの実行などの観察可能な副作用をアサートします。 このスキルは、`react-reviewer`や`tdd-guide`といった関連エージェントと連携し、コードレビュー時のテスト品質評価やTDDプロセスの強制を支援します。

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

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

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

ドキュメント

React Testing は何をしますか?

Reactコンポーネントのテストに関する包括的なパターン集。動作重視のコンポーネントテスト、カスタムフックのテスト、アクセシビリティアサーション、ネットワークレベルのモックを網羅します。

アクティベートするタイミング

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

中核となる原則

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

テストは以下のことを行うべき:

  • コンポーネントを本番環境と同じプロバイダでレンダリングする
  • アクセシブルなクエリ(role, label)と userEvent を使って操作する
  • 目に見える出力と観察可能な副作用(コールバックの実行、リクエストの送信)をアサートする

テストは以下のことを行うべきではない:

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

ライブラリの選択

ランナー使用タイミング備考
VitestVite、Remix、モダンなセットアップ高速、ネイティブESM、Jest互換API
JestNext.js、CRA、確立されたリポジトリ多くのReactプロジェクトのデフォルト
Playwright Component Testing実際のブラウザエンジンが必要な場合JSDOMに必要な機能がない場合に使用
Cypress Component Testing実際のブラウザ、Cypressを既に使用している場合Playwright CTの代替

ひとつを選んでください。明確なレーン分けがない限り、同じリポジトリ内で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() を一度呼び出し、返された 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はネットワーク層でモックします。コンポーネント、フック、fetchライブラリはすべて本番環境とまったく同じように動作します。

セットアップ

// 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" />);
  expect(await screen.findByText(/something went wrong/i)).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をラッパーの外側でテストごとに一度だけインスタンス化し、再レンダリングを跨いでキャッシュを維持する。
  // ラッパーのクロージャ内で作成すると、レンダリングのたびにキャッシュがリセットされ、フレーキーなテストになる。
  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} />);
  expect(await axe(container)).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 Component Testing(実際のブラウザでのコンポーネントテスト)またはフル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. コンポーネントのプロップタイプとシグネチャを定義する
  2. 最も単純なケースの最初のテストを書く
  3. 正しい理由で失敗することを確認する
  4. 通すのに十分なだけ実装する
  5. 次のテストケースを追加する
  6. 3番目の類似テストがパターンを明らかにしたらリファクタリングする

テストコマンド

# Vitest
vitest                            # ウォッチモード
vitest run                        # 一度だけ実行
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 }));

  expect(await screen.findByText(/saved successfully/i)).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();
  expect(await screen.findByText("Alice")).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/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

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.

関連スキル