Communitygithub.com

React测试

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

React测试 是什么?

这是一套全面的React测试模式指南,专注于行为驱动的组件测试、自定义Hook测试、无障碍断言以及网络层模拟。它详细介绍了React Testing Library的核心原则(测试用户所见和所操作的内容,而非实现细节),并提供了从查询优先级、用户交互(userEvent)、异步模式到MSW网络模拟的完整示例。该技能还涵盖了Provider封装、自定义Hook测试、无障碍断言(axe)、快照测试的合理使用场景,以及明确何时应转向Playwright/Cypress进行端到端测试的决策边界。支持Vitest和Jest两种测试运行器,并提供了TDD工作流和覆盖率目标配置。

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

在你喜欢的 AI 中提问

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

文档

React测试 是做什么的?

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

何时激活

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

核心原则

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

测试应该:

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

测试不应该:

  • 检查组件状态、传递给子组件的props或调用了哪些Hook
  • 模拟React本身或框架Hook
  • 断言渲染次数或影响用户的DOM结构之外的细节

库选择

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

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

查询优先级

React Testing Library 暴露了三个层次的查询方法——请自上而下使用:

  1. 对所有人可访问的:getByRole、getByLabelText、getByPlaceholderText、getByText、getByDisplayValue
  2. 语义的:getByAltText、getByTitle
  3. Test 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]" });
});
  • 始终对userEvent调用使用await
  • 每个测试调用一次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 () => {
  // 在wrapper外部每个测试只实例化一次QueryClient,以便它能在重新渲染时保持状态。
  // 在wrapper闭包内创建它会在每次渲染时重置缓存状态,导致测试不稳定。
  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进行测试
  • 对于使用Context的Hook,传递wrapper

无障碍断言

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

test("UserCard 没有a11y违规", async () => {
  const { container } = render(<UserCard user={mockUser} />);
  expect(await axe(container)).toHaveNoViolations();
});

在组件测试中为每个交互式组件运行axe。它能捕获:

  • 表单输入缺少标签
  • 无效的ARIA用法
  • 颜色对比度差(有限制——JSDOM没有真正的CSS引擎,因此仅对内联样式有效;视觉对比度属于Playwright)
  • 图片缺少替代文本
  • 标题顺序违规

交叉链接: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技能。

决策边界:

  • 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输出(对于预期的抛出是正常的),然后恢复
  // 以便间谍不会泄漏到其他测试并隐藏真实的错误。
  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.

相关技能