关于 react-testing-library
面向 React Testing Library 的测试参考技能,指导从用户视角编写测试,涵盖查询、交互、异步行为与调试。
主要功能
- 01无障碍查询优先级
- 02按匹配和时序选择查询
- 03用户事件交互模式
- 04异步断言模式
- 05Hook 与 Provider 测试
- 06DOM 调试与配置
适用场景
- 01为无障碍界面测试选择查询方式
- 02测试异步内容与交互
- 03使用 Provider 测试 Hook 和组件
- 04诊断渲染 DOM 的测试失败
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ itechmeat/ llm-code --skill react-testing-library安装后在 Claude Code 中输入/react-testing-library确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api.mdasync.mdconfig.mddebugging.mdqueries.mduser-events.md详细介绍
React Testing Library 技能
快速导航
| 主题 | 链接 |
|---|---|
| 查询 | references/queries.md |
| 用户事件 | references/user-events.md |
| API | references/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