react-testing-library

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Testing Library Skill

React Testing Library 技能手册

Quick Navigation

快速导航

TopicLink
Queriesreferences/queries.md
User Eventsreferences/user-events.md
APIreferences/api.md
Asyncreferences/async.md
Debuggingreferences/debugging.md
Configreferences/config.md

主题链接
查询方法references/queries.md
用户事件references/user-events.md
APIreferences/api.md
异步处理references/async.md
调试references/debugging.md
配置references/config.md

Installation

安装

bash
undefined
bash
undefined

Core (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

查询类型

TypeNo Match1 Match>1 MatchAsync
getBy...
throwreturnthrowNo
queryBy...
nullreturnthrowNo
findBy...
throwreturnthrowYes
getAllBy...
throwarrayarrayNo
queryAllBy...
[]arrayarrayNo
findAllBy...
throwarrayarrayYes
When to use:
  • getBy*
    — element exists
  • queryBy*
    — element may not exist (assertions like
    expect(...).not.toBeInTheDocument()
    )
  • findBy*
    — element appears asynchronously

类型无匹配项1个匹配项多个匹配项异步
getBy...
抛出错误返回元素抛出错误
queryBy...
返回null返回元素抛出错误
findBy...
抛出错误返回元素抛出错误
getAllBy...
抛出错误返回数组返回数组
queryAllBy...
返回[]返回数组返回数组
findAllBy...
抛出错误返回数组返回数组
使用场景
  • 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
@testing-library/user-event
over
fireEvent
:
ts
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-event
而非
fireEvent
ts
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 length

ts
// 打印整个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

快速参考

ImportUsage
render
Render component to DOM
screen
Query the rendered DOM
cleanup
Unmount components (auto in Jest)
act
Wrap state updates
renderHook
Test custom hooks
within
Scope queries to element
waitFor
Retry until assertion passes
configure
Set global options
userEvent.setup()
Create user event instance
导入项用途
render
将组件渲染到DOM中
screen
查询渲染后的DOM
cleanup
卸载组件(Jest中自动执行)
act
包裹状态更新操作
renderHook
测试自定义Hook
within
将查询范围限定到特定元素
waitFor
重试直到断言通过
configure
设置全局选项
userEvent.setup()
创建用户事件实例