Communitygithub.com

React 测试

使用 React Testing Library、Vitest/Jest、MSW 进行网络模拟、axe 进行无障碍断言,以及组件测试与 Playwright/Cypress 端到端测试之间的决策边界。适用于编写或修复 React 组件、Hooks 或页面的测试。

React 测试 是什么?

本技能提供了全面的 React 测试指南,专注于行为驱动的组件测试、自定义 Hooks 测试、无障碍断言以及网络层模拟。它强调测试用户可见和可操作的内容,而非实现细节,并推荐使用 React Testing Library 搭配 `userEvent` 进行用户交互模拟。技能详细介绍了查询优先级、异步模式、使用 MSW 进行网络请求模拟、Provider 包装、自定义 Hooks 测试以及无障碍断言(axe)。此外,还涵盖了测试覆盖目标、反模式、TDD 工作流,并清晰划定了在 JSDOM(Vitest/Jest)中测试与使用 Playwright/Cypress 进行真实浏览器测试的边界。该技能适用于 Vitest 和 Jest 两种运行环境,并提供了从设置到高级用例(如错误边界和 Suspense 测试)的完整示例。

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

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

React 测试 是做什么的?

全面的 React 测试模式,涵盖行为驱动的组件测试、自定义 Hook 测试、无障碍断言和网络层模拟。

何时激活

  • 为 React 组件、自定义 Hook 或页面编写测试
  • 为遗留的未测试组件添加测试覆盖
  • 从 Enzyme 或类组件时代的模式迁移到 React Testing Library
  • 为新 React 项目设置 Vitest 或 Jest
  • 在测试中模拟 HTTP 请求
  • 断言无障碍违规
  • 决定哪些测试属于 RTL、Playwright 组件测试或完整 E2E

核心原则

测试用户所看到和操作的内容,而非实现细节。

一个测试应当:

  • 使用与生产环境相同的 Provider 渲染组件
  • 通过无障碍查询(role, label)和 userEvent 与之交互
  • 断言可见的输出和可观察的副作用(回调被调用,请求已发送)

一个测试不应当:

  • 检查组件的 state、传递给子组件的 props 或在哪个 Hook 被调用
  • 模拟 React 本身或框架 Hook
  • 断言渲染次数或超出用户影响的 DOM 结构

测试库选择

运行器适用场景备注
VitestVite、Remix、现代项目更快,原生 ESM,兼容 Jest API
JestNext.js、CRA、既有仓库许多 React 项目的默认选择
Playwright 组件测试需要真实浏览器引擎当 JSDOM 缺少所需功能时使用
Cypress 组件测试真实浏览器,且已在使用 CypressPlaywright CT 的替代方案

选择一个。除非有明确的分层分离,否则不要在同一个仓库中同时运行 RTL + Vitest 和 Playwright CT。

查询优先级

React Testing Library 提供了三个层级的查询——请从上到下使用:

  1. 所有用户可访问:getByRole、getByLabelText、getByPlaceholderText、getByText、getByDisplayValue
  2. 语义:getByAltText、getByTitle
  3. 测试 ID(逃生舱):getByTestId
// 最佳
screen.getByRole("button", { name: /save/i });

// 输入框可行
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]" });
});
  • 始终 await userEvent 调用
  • 每个测试调用一次 userEvent.setup(),重复使用返回的 user
  • userEvent 模拟真实的浏览器序列;fireEvent 只分发一个合成事件——优先使用 userEvent

异步模式

// 在异步工作后出现的元素
expect(await screen.findByText("Loaded")).toBeInTheDocument();

// 副作用断言
await waitFor(() => expect(saveSpy).toHaveBeenCalled());

// 应该消失的元素
await waitForElementToBeRemoved(() => screen.queryByText("Loading"));

永远不要使用 setTimeout + 断言——不稳定。请使用上述匹配器。

使用 MSW 进行网络模拟

Mock Service Worker 在网络层进行模拟。组件、Hook 和 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();
});

Provider 包装

在 test-utils.tsx 中统一包装 Provider:

// 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"。

自定义 Hook 测试

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 中
  • 仅通过 Hook 的公共 API 进行测试
  • 对于使用上下文的 Hook,传递一个 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 组件测试(在真实浏览器中的组件测试)或完整 E2E。请参阅 e2e-testing skill。

决策边界:

  • Hook、展示性组件、带有逻辑的表单 -> RTL
  • 其布局很重要或使用 JSDOM 中没有的浏览器 API 的组件 -> Playwright CT
  • 跨多个页面的完整用户流程 -> Playwright/Cypress E2E

覆盖目标

层目标
纯工具函数>=90%
自定义 Hook>=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() 警告 — 它们指示真正的 bug(卸载后状态更新、缺少异步包装)
  • 跨测试共享可变状态 — 当测试顺序改变时导致不稳定
  • 移除 it.skip() 后通过的测试 — 你的测试并没有真正断言你认为的内容

TDD 工作流

RED     -> 为下一个需求编写失败的测试
GREEN   -> 编写最少的组件代码以通过测试
REFACTOR -> 改进组件,测试保持绿色
REPEAT  -> 下一个需求

对于新组件:

  1. 定义组件的 prop 类型和签名
  2. 为最简单的情况编写第一个测试
  3. 验证它因正确的理由而失败
  4. 实现刚好足够的代码以通过
  5. 添加下一个测试用例
  6. 当第三个相似的测试揭示一个模式时进行重构

测试命令

# 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("错误边界渲染后备 UI", () => {
  // 抑制 React 对预期异常的 console.error 噪音,然后恢复
  // 以便 spy 不会泄漏到其他测试并隐藏真正的错误。
  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/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.

相关技能