react-testing-library
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Testing Library Skill
React Testing Library 技能手册
Quick Navigation
快速导航
| Topic | Link |
|---|---|
| Queries | references/queries.md |
| User Events | references/user-events.md |
| API | references/api.md |
| Async | references/async.md |
| Debugging | references/debugging.md |
| Config | references/config.md |
| 主题 | 链接 |
|---|---|
| 查询方法 | references/queries.md |
| 用户事件 | references/user-events.md |
| API | references/api.md |
| 异步处理 | references/async.md |
| 调试 | references/debugging.md |
| 配置 | references/config.md |
Installation
安装
bash
undefinedbash
undefinedCore (v16+: @testing-library/dom is peer dependency)
Core (v16+: @testing-library/dom is peer dependency)
npm install --save-dev @testing-library/react @testing-library/dom
npm install --save-dev @testing-library/react @testing-library/dom
TypeScript support
TypeScript support
npm install --save-dev @types/react @types/react-dom
npm install --save-dev @types/react @types/react-dom
Recommended: user-event for interactions
Recommended: user-event for interactions
npm install --save-dev @testing-library/user-event
npm install --save-dev @testing-library/user-event
Recommended: jest-dom for matchers
Recommended: jest-dom for matchers
npm install --save-dev @testing-library/jest-dom
**React 19 support**: Requires `@testing-library/react` v16.1.0+npm install --save-dev @testing-library/jest-dom
**React 19 支持**:需要 `@testing-library/react` v16.1.0+Core Philosophy
核心理念
"The more your tests resemble the way your software is used, the more confidence they can give you."
Avoid testing:
- Internal state of components
- Internal methods
- Lifecycle methods
- Child component implementation details
Test instead:
- What users see and interact with
- Behavior from user's perspective
- Accessibility (queries by role, label)
"你的测试越贴近软件的实际使用方式,就能给你带来越高的信心。"
避免测试:
- 组件的内部状态
- 内部方法
- 生命周期方法
- 子组件的实现细节
应该测试:
- 用户能看到和交互的内容
- 从用户视角出发的行为
- 无障碍性(通过角色、标签进行查询)
Query Priority
查询优先级
Use queries in this order of preference:
请按以下优先级使用查询方法:
1. Accessible to Everyone (Preferred)
1. 面向所有用户(首选)
ts
// Best — by ARIA role
getByRole("button", { name: /submit/i });
getByRole("textbox", { name: /email/i });
// Form fields — by label
getByLabelText("Email");
// Non-interactive content — by text
getByText("Welcome back!");ts
// 最佳方式 — 通过ARIA角色
getByRole("button", { name: /submit/i });
getByRole("textbox", { name: /email/i });
// 表单字段 — 通过标签
getByLabelText("Email");
// 非交互式内容 — 通过文本
getByText("Welcome back!");2. Semantic Queries
2. 语义化查询
ts
// Images
getByAltText("Company logo");
// Title attribute (less reliable)
getByTitle("Close");ts
// 图片
getByAltText("Company logo");
// 标题属性(可靠性较低)
getByTitle("Close");3. Test IDs (Escape Hatch)
3. 测试ID(应急方案)
ts
// Only when other queries don't work
getByTestId("custom-element");ts
// 仅当其他查询方法不适用时使用
getByTestId("custom-element");Query Types
查询类型
| Type | No Match | 1 Match | >1 Match | Async |
|---|---|---|---|---|
| throw | return | throw | No |
| null | return | throw | No |
| throw | return | throw | Yes |
| throw | array | array | No |
| [] | array | array | No |
| throw | array | array | Yes |
When to use:
- — element exists
getBy* - — element may not exist (assertions like
queryBy*)expect(...).not.toBeInTheDocument() - — element appears asynchronously
findBy*
| 类型 | 无匹配项 | 1个匹配项 | 多个匹配项 | 异步 |
|---|---|---|---|---|
| 抛出错误 | 返回元素 | 抛出错误 | 否 |
| 返回null | 返回元素 | 抛出错误 | 否 |
| 抛出错误 | 返回元素 | 抛出错误 | 是 |
| 抛出错误 | 返回数组 | 返回数组 | 否 |
| 返回[] | 返回数组 | 返回数组 | 否 |
| 抛出错误 | 返回数组 | 返回数组 | 是 |
使用场景:
- — 确定元素存在时
getBy* - — 元素可能不存在时(如断言
queryBy*)expect(...).not.toBeInTheDocument() - — 元素异步出现时
findBy*
Basic Test Pattern
基础测试模式
tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("shows greeting after login", async () => {
const user = userEvent.setup();
render(<App />);
// Act — simulate user interactions
await user.type(screen.getByLabelText(/username/i), "john");
await user.click(screen.getByRole("button", { name: /login/i }));
// Assert — verify outcome
expect(await screen.findByText(/welcome, john/i)).toBeInTheDocument();
});tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("shows greeting after login", async () => {
const user = userEvent.setup();
render(<App />);
// 操作 — 模拟用户交互
await user.type(screen.getByLabelText(/username/i), "john");
await user.click(screen.getByRole("button", { name: /login/i }));
// 断言 — 验证结果
expect(await screen.findByText(/welcome, john/i)).toBeInTheDocument();
});User Events
用户事件
Always use over :
@testing-library/user-eventfireEventts
import userEvent from "@testing-library/user-event";
test("user interactions", async () => {
const user = userEvent.setup();
// Click
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element);
// Type
await user.type(input, "Hello");
await user.clear(input);
// Select
await user.selectOptions(select, ["option1", "option2"]);
// Keyboard
await user.keyboard("{Enter}");
await user.keyboard("[ShiftLeft>]a[/ShiftLeft]"); // Shift+A
// Clipboard
await user.copy();
await user.paste();
// Pointer
await user.hover(element);
await user.unhover(element);
});请始终使用 而非 :
@testing-library/user-eventfireEventts
import userEvent from "@testing-library/user-event";
test("user interactions", async () => {
const user = userEvent.setup();
// 点击
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element);
// 输入
await user.type(input, "Hello");
await user.clear(input);
// 选择
await user.selectOptions(select, ["option1", "option2"]);
// 键盘操作
await user.keyboard("{Enter}");
await user.keyboard("[ShiftLeft>]a[/ShiftLeft]"); // Shift+A
// 剪贴板操作
await user.copy();
await user.paste();
// 指针操作
await user.hover(element);
await user.unhover(element);
});Async Patterns
异步模式
waitFor — Retry Until Success
waitFor — 重试直到成功
ts
await waitFor(() => {
expect(screen.getByText("Loaded")).toBeInTheDocument();
});
// With options
await waitFor(() => expect(callback).toHaveBeenCalled(), {
timeout: 5000,
interval: 100,
});ts
await waitFor(() => {
expect(screen.getByText("Loaded")).toBeInTheDocument();
});
// 带配置选项
await waitFor(() => expect(callback).toHaveBeenCalled(), {
timeout: 5000,
interval: 100,
});findBy — Built-in waitFor
findBy — 内置的waitFor
ts
// Equivalent to: await waitFor(() => getByText('Loaded'))
const element = await screen.findByText("Loaded");ts
// 等价于: await waitFor(() => getByText('Loaded'))
const element = await screen.findByText("Loaded");waitForElementToBeRemoved
waitForElementToBeRemoved
ts
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));ts
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));Common Patterns
常见模式
Custom Render with Providers
带Provider的自定义渲染
tsx
// test-utils.tsx
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";
import { AuthProvider } from "./AuthProvider";
function AllProviders({ children }) {
return (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
);
}
const customRender = (ui, options) => render(ui, { wrapper: AllProviders, ...options });
export * from "@testing-library/react";
export { customRender as render };tsx
// test-utils.tsx
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";
import { AuthProvider } from "./AuthProvider";
function AllProviders({ children }) {
return (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
);
}
const customRender = (ui, options) => render(ui, { wrapper: AllProviders, ...options });
export * from "@testing-library/react";
export { customRender as render };Testing Hooks
测试自定义Hook
ts
import { renderHook, act } from "@testing-library/react";
test("useCounter increments", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});ts
import { renderHook, act } from "@testing-library/react";
test("useCounter increments", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Rerender with New Props
重新渲染并传入新Props
ts
const { rerender } = render(<Counter count={1} />);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
rerender(<Counter count={2} />);
expect(screen.getByText("Count: 2")).toBeInTheDocument();ts
const { rerender } = render(<Counter count={1} />);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
rerender(<Counter count={2} />);
expect(screen.getByText("Count: 2")).toBeInTheDocument();Query Within Container
在容器内查询
ts
import { within } from "@testing-library/react";
const modal = screen.getByRole("dialog");
const submitBtn = within(modal).getByRole("button", { name: /submit/i });ts
import { within } from "@testing-library/react";
const modal = screen.getByRole("dialog");
const submitBtn = within(modal).getByRole("button", { name: /submit/i });Debugging
调试
ts
// Print entire DOM
screen.debug();
// Print specific element
screen.debug(screen.getByRole("button"));
// Log available roles
import { logRoles } from "@testing-library/react";
logRoles(container);
// With prettyDOM options
screen.debug(undefined, 10000); // max lengthts
// 打印整个DOM
screen.debug();
// 打印特定元素
screen.debug(screen.getByRole("button"));
// 记录可用角色
import { logRoles } from "@testing-library/react";
logRoles(container);
// 带prettyDOM选项
screen.debug(undefined, 10000); // 最大长度jest-dom Matchers
jest-dom 匹配器
ts
import "@testing-library/jest-dom";
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEnabled();
expect(element).toBeDisabled();
expect(element).toHaveTextContent("Hello");
expect(element).toHaveValue("input value");
expect(element).toHaveAttribute("href", "/home");
expect(element).toHaveClass("active");
expect(element).toHaveFocus();
expect(element).toBeChecked();ts
import "@testing-library/jest-dom";
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEnabled();
expect(element).toBeDisabled();
expect(element).toHaveTextContent("Hello");
expect(element).toHaveValue("input value");
expect(element).toHaveAttribute("href", "/home");
expect(element).toHaveClass("active");
expect(element).toHaveFocus();
expect(element).toBeChecked();Configuration
配置
ts
import { configure } from "@testing-library/react";
configure({
// Custom test ID attribute
testIdAttribute: "data-my-test-id",
// Async timeout
asyncUtilTimeout: 5000,
// Default hidden
defaultHidden: true,
// Throw suggestions (debugging)
throwSuggestions: true,
});ts
import { configure } from "@testing-library/react";
configure({
// 自定义测试ID属性
testIdAttribute: "data-my-test-id",
// 异步超时时间
asyncUtilTimeout: 5000,
// 默认隐藏元素
defaultHidden: true,
// 抛出建议(调试用)
throwSuggestions: true,
});❌ Prohibitions (Anti-patterns)
❌ 禁止操作(反模式)
ts
// ❌ Don't query by class/id
container.querySelector(".my-class");
// ❌ Don't use container.firstChild
const { container } = render(<Component />);
expect(container.firstChild).toHaveClass("active");
// ❌ Don't use fireEvent when userEvent works
fireEvent.click(button); // Use userEvent.click instead
// ❌ Don't test implementation details
expect(component.state.loading).toBe(false);
// ❌ Don't use waitFor with findBy
await waitFor(() => screen.findByText("x")); // findBy already waits
// ❌ Don't assert inside waitFor callback (unless necessary)
await waitFor(() => {
expect(mockFn).toHaveBeenCalled(); // OK - need to wait for call
});ts
// ❌ 不要通过类名/ID查询
container.querySelector(".my-class");
// ❌ 不要使用container.firstChild
const { container } = render(<Component />);
expect(container.firstChild).toHaveClass("active");
// ❌ 当userEvent可用时不要使用fireEvent
fireEvent.click(button); // 请改用userEvent.click
// ❌ 不要测试实现细节
expect(component.state.loading).toBe(false);
// ❌ 不要在findBy中使用waitFor
await waitFor(() => screen.findByText("x")); // findBy本身已包含等待逻辑
// ❌ 除非必要,否则不要在waitFor回调中使用断言
await waitFor(() => {
expect(mockFn).toHaveBeenCalled(); // 可以 - 需要等待调用完成
});✅ Best Practices
✅ 最佳实践
ts
// ✅ Use screen for all queries
import { render, screen } from "@testing-library/react";
render(<Component />);
screen.getByRole("button"); // Good
// ✅ Prefer userEvent over fireEvent
const user = userEvent.setup();
await user.click(button);
// ✅ Use findBy for async elements
const element = await screen.findByText("Loaded");
// ✅ Use queryBy for non-existence assertions
expect(screen.queryByText("Error")).not.toBeInTheDocument();
// ✅ Use within for scoped queries
const form = screen.getByRole("form");
within(form).getByLabelText("Email");
// ✅ Use accessible queries (role, label, text)
getByRole("button", { name: /submit/i });ts
// ✅ 使用screen进行所有查询
import { render, screen } from "@testing-library/react";
render(<Component />);
screen.getByRole("button"); // 推荐
// ✅ 优先使用userEvent而非fireEvent
const user = userEvent.setup();
await user.click(button);
// ✅ 对异步元素使用findBy
const element = await screen.findByText("Loaded");
// ✅ 对不存在的元素使用queryBy
expect(screen.queryByText("Error")).not.toBeInTheDocument();
// ✅ 使用within进行范围查询
const form = screen.getByRole("form");
within(form).getByLabelText("Email");
// ✅ 使用无障碍查询(角色、标签、文本)
getByRole("button", { name: /submit/i });TextMatch Options
TextMatch 选项
ts
// Exact match (default)
getByText("Hello World");
// Substring match
getByText("llo Worl", { exact: false });
// Regex
getByText(/hello world/i);
// Custom function
getByText((content, element) => {
return element.tagName === "SPAN" && content.startsWith("Hello");
});ts
// 精确匹配(默认)
getByText("Hello World");
// 子串匹配
getByText("llo Worl", { exact: false });
// 正则表达式
getByText(/hello world/i);
// 自定义函数
getByText((content, element) => {
return element.tagName === "SPAN" && content.startsWith("Hello");
});Quick Reference
快速参考
| Import | Usage |
|---|---|
| Render component to DOM |
| Query the rendered DOM |
| Unmount components (auto in Jest) |
| Wrap state updates |
| Test custom hooks |
| Scope queries to element |
| Retry until assertion passes |
| Set global options |
| Create user event instance |
| 导入项 | 用途 |
|---|---|
| 将组件渲染到DOM中 |
| 查询渲染后的DOM |
| 卸载组件(Jest中自动执行) |
| 包裹状态更新操作 |
| 测试自定义Hook |
| 将查询范围限定到特定元素 |
| 重试直到断言通过 |
| 设置全局选项 |
| 创建用户事件实例 |