测试
React 组件测试推荐 React Testing Library(RTL):从用户可见行为断言,而非实现细节。运行器常用 Vitest 或 Jest。
核心概念
RTL 的设计哲学是**「测试软件的使用方式越接近用户的使用方式,测试就越可靠」**。它不提供直接访问组件内部 state/props 的 API,而是强制你通过 DOM 查询(按角色、文本、标签)和模拟用户交互(点击、输入)来编写测试——这样测试更贴近真实使用场景,也更能容忍内部实现的重构。
安装(Vitest 示例)
pnpm add -D vitest @testing-library/react @testing-library/jest-dom jsdom
vite.config.ts 中配置 test.environment: "jsdom"(见 Vitest 文档):
// vite.config.ts
export default defineConfig({
test: {
environment: "jsdom",
setupFiles: "./src/test/setup.ts",
globals: true,
},
});
// src/test/setup.ts
import "@testing-library/jest-dom/vitest";
基本用例
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { Counter } from "./Counter";
describe("Counter", () => {
it("点击后计数增加", async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole("button", { name: /加一/i }));
expect(screen.getByText("1")).toBeInTheDocument();
});
});
优先使用 role、label、text 查询(getByRole、getByLabelText),与可访问性一致。
查询方法优先级
RTL 官方建议按以下优先级选择查询方式(越靠前越推荐):
getByRole:按 ARIA 角色(button、textbox、heading),最贴近用户/屏幕阅读器的感知方式。getByLabelText:表单控件配合<label>。getByPlaceholderText/getByText:无更好选择时的次优方案。getByTestId:最后手段,仅当以上都不适用(如没有语义化角色的容器)。
// 推荐
screen.getByRole("textbox", { name: "邮箱" });
// 避免(除非无法用语义化查询)
screen.getByTestId("email-input");
测试带依赖注入的组件
function renderWithProviders(ui: ReactNode) {
return render(
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider>{ui}</ThemeProvider>
</QueryClientProvider>
);
}
it("展示用户名", async () => {
renderWithProviders(<UserProfile userId="1" />);
expect(await screen.findByText("Test User")).toBeInTheDocument();
});
抽出 renderWithProviders 帮助函数,避免每个测试文件重复包裹相同的 Context/Provider。
异步与副作用
findBy*:等待元素出现(内部封装了waitFor,适合断言异步渲染结果)。waitFor:等待断言条件。- 涉及
useEffect的请求可用 MSW 模拟网络:
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
const server = setupServer(
http.get("/api/users/:id", () => HttpResponse.json({ id: "1", name: "Test User" }))
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
MSW 在网络层拦截请求,比手动 mock fetch 更真实,也能在开发环境复用同一套 mock 定义。
快照测试的取舍
it("匹配快照", () => {
const { asFragment } = render(<Badge type="success">已完成</Badge>);
expect(asFragment()).toMatchSnapshot();
});
快照测试对小型、稳定的展示组件(如 Badge、Icon)有一定价值,但大段 DOM 快照难以人工审查、容易被无脑更新("snapshot rubber-stamping"),核心业务逻辑应优先用行为断言而非快照。
避免
- 依赖组件内部 state 变量名或私有方法。
- 快照测试滥用(大段 DOM 快照难以维护)。
- 用
container.querySelector代替语义化查询(除非确实没有更好的方式)。
最佳实践
- 测试文件贴近「用户故事」组织:
it("输入非法邮箱后显示错误提示"),而不是it("email state 更新")。 - 表单类组件重点测校验逻辑与提交结果,不测内部
useState的字段名。 - 网络请求统一用 MSW 模拟,同一套 handlers 可以在开发环境(mock server)和测试中复用。
- 组件依赖多个 Context/Provider 时,封装
renderWithProviders减少重复代码。 - CI 上锁定 Node 版本与依赖版本,避免 jsdom/浏览器行为差异导致的偶发失败。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
getByRole 找不到元素但页面上明明有 | 元素缺少正确的 ARIA role 或语义化标签 | 检查是否用了语义化 HTML(<button> 而非 <div onClick>) |
| 测试报「更新未包裹在 act(...) 中」 | 异步状态更新未被测试工具等待完成 | 用 findBy*/waitFor 等待更新完成,而非同步断言 |
| Mock 请求不生效 | MSW handler 与实际请求 URL/方法不匹配 | 核对拦截规则的 path 与真实请求一致 |
| 快照测试频繁失败又被无脑更新 | 快照过大、覆盖了不相关的样式细节 | 缩小快照范围或改用具体断言 |
| 测试之间状态互相影响 | 全局 mock(如 MSW handlers)未在每个测试后重置 | 用 afterEach 清理/重置 mock 状态 |
延伸阅读
- Effect 与副作用:需要 mock 的典型场景
- Context:测试时如何包裹 Provider
- 工程化:Vite 测试环境配置
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Testing Library:React | RTL 指南 |
| Vitest 文档 | 测试运行器 |
| React:测试 Recipes | 官方概览 |
| MSW | 网络请求 Mock |
| Testing Library:查询优先级 | 查询方式指南 |
相关文章
渲染与协调
React 将组件渲染为虚拟 DOM 树,再协调(reconciliation)到浏览器 DOM。理解该过程有助于解释 key、memo 与性能优化。涵盖渲染流程、Fiber、startTransition、Suspense、最佳实践与常见坑。
工程化
新建 React 项目时,官方与社区主流工具为 Vite;全栈场景常用 Next.js。涵盖 Vite + React、Create React App 现状、Next.js、目录结构、环境变量、部署、最佳实践与常见坑。
TypeScript
React 官方推荐 TypeScript。类型主要落在 Props、事件、Ref 与 Hooks 返回值上。见 组件。
构建与分包
通过代码分割与构建配置减小首屏 JavaScript 体积。涵盖 React.lazy/Suspense、路由级分割、Tree Shaking、Vite 生产构建、体积分析、最佳实践与常见坑。运行时优化见 runtime。
Effect 与副作用
useEffect 用于在组件渲染后执行与外部系统同步的逻辑(请求、订阅、手动改 DOM 等)。涵盖基本用法、依赖数组、适用/不适用场景、useLayoutEffect、最佳实践与常见坑。
状态管理
React 组件内 state 用 Hooks;跨组件共享状态可选 Context、Redux Toolkit、Zustand 等。见 Context Hook。
Series
react
15 / 16