mcpField
R
react-testing-library

react-testing-library

github.com/itechmeat测试与质量241.4k 次安装4 天前更新
代码仓库

关于 react-testing-library

面向 React Testing Library 的测试参考技能,指导从用户视角编写测试,涵盖查询、交互、异步行为与调试。

主要功能

  1. 01无障碍查询优先级
  2. 02按匹配和时序选择查询
  3. 03用户事件交互模式
  4. 04异步断言模式
  5. 05Hook 与 Provider 测试
  6. 06DOM 调试与配置

适用场景

  1. 01为无障碍界面测试选择查询方式
  2. 02测试异步内容与交互
  3. 03使用 Provider 测试 Hook 和组件
  4. 04诊断渲染 DOM 的测试失败

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/itechmeat/llm-code --skill react-testing-library

安装后在 Claude Code 中输入/react-testing-library确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
api.md
async.md
config.md
debugging.md
queries.md
user-events.md

详细介绍

React Testing Library 技能

快速导航

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

安装

安装:npm install --save-dev @testing-library/react @testing-library/dom。推荐安装的额外依赖:@testing-library/user-event 和 @testing-library/jest-dom。React 19 需要 v16.1.0+。

核心理念

“测试越接近用户使用软件的方式,就越能增强你的信心。”

避免测试:

  • 组件的内部状态
  • 内部方法
  • 生命周期方法
  • 子组件的实现细节

应测试:

  • 用户看到并交互的内容
  • 从用户视角出发的行为
  • 可访问性(按角色、标签查询)

查询优先级

按以下优先顺序使用查询:

1. 所有人都能访问的查询(首选)
// 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!");
2. 语义查询
// Images
getByAltText("Company logo");

// Title attribute (less reliable)
getByTitle("Close");
3. 测试 ID(备用方案)
// Only when other queries don't work
getByTestId("custom-element");

查询类型

类型无匹配项1 个匹配项>1 个匹配项异步
getBy...抛出异常返回抛出异常否
queryBy...null返回抛出异常否
findBy...抛出异常返回抛出异常是
getAllBy...抛出异常数组数组否
queryAllBy...[]数组数组否
findAllBy...抛出异常数组数组是

使用场景:

  • getBy* — 元素存在
  • queryBy* — 元素可能不存在(例如 expect(...).not.toBeInTheDocument() 这样的断言)
  • findBy* — 元素异步出现

基本测试模式

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();
});

用户事件

始终优先使用 @testing-library/user-event,而不是 fireEvent:

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);
});

异步模式

waitFor — 重试直到成功
await waitFor(() => {
  expect(screen.getByText("Loaded")).toBeInTheDocument();
});

// With options
await waitFor(() => expect(callback).toHaveBeenCalled(), {
  timeout: 5000,
  interval: 100,
});
findBy — 内置 waitFor
// Equivalent to: await waitFor(() => getByText('Loaded'))
const element = await screen.findByText("Loaded");
waitForElementToBeRemoved
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));

常见模式

使用 Providers 自定义渲染
// 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 };
测试 Hooks
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);
});
使用新 Props 重新渲染
const { rerender } = render(<Counter count={1} />);
expect(screen.getByText("Count: 1")).toBeInTheDocument();

rerender(<Counter count={2} />);
expect(screen.getByText("Count: 2")).toBeInTheDocument();
在容器内查询
import { within } from "@testing-library/react";

const modal = screen.getByRole("dialog");
const submitBtn = within(modal).getByRole("button", { name: /submit/i });

调试

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

jest-dom 匹配器

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();

配置

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,
});

❌ 禁止事项(反模式)

// ❌ 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
});

✅ 最佳实践

// ✅ 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 });

TextMatch 选项

// 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");
});

快速参考

导入用途
render将组件渲染到 DOM
screen查询已渲染的 DOM
cleanup卸载组件(在 Jest 中自动执行)
act包装状态更新
renderHook测试自定义 Hooks
within将查询范围限定在元素内
waitFor重试直到断言通过
configure设置全局选项
userEvent.setup()创建用户事件实例

链接

评论

支持 Markdown