react-testing-library by itechmeat/llm-code
npx skills add https://github.com/itechmeat/llm-code --skill react-testing-library| 主题 | 链接 |
|---|---|
| 查询方法 | references/queries.md |
| 用户事件 | references/user-events.md |
| API | references/api.md |
| 异步操作 | references/async.md |
| 调试 | references/debugging.md |
| 配置 | references/config.md |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
安装:npm install --save-dev @testing-library/react @testing-library/dom。推荐额外安装:@testing-library/user-event 和 @testing-library/jest-dom。React 19 需要 v16.1.0+ 版本。
"你的测试越接近软件的实际使用方式,它们能给你的信心就越大。"
避免测试:
应该测试:
按以下优先顺序使用查询方法:
// 最佳 — 通过 ARIA 角色
getByRole("button", { name: /submit/i });
getByRole("textbox", { name: /email/i });
// 表单字段 — 通过标签
getByLabelText("Email");
// 非交互内容 — 通过文本
getByText("Welcome back!");
// 图片
getByAltText("Company logo");
// 标题属性(可靠性较低)
getByTitle("Close");
// 仅当其他查询方法无效时使用
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 />);
// 操作 — 模拟用户交互
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();
});
始终使用 @testing-library/user-event 而非 fireEvent:
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);
});
await waitFor(() => {
expect(screen.getByText("Loaded")).toBeInTheDocument();
});
// 带选项
await waitFor(() => expect(callback).toHaveBeenCalled(), {
timeout: 5000,
interval: 100,
});
// 等同于:await waitFor(() => getByText('Loaded'))
const element = await screen.findByText("Loaded");
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
// 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 };
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);
});
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 });
// 打印整个 DOM
screen.debug();
// 打印特定元素
screen.debug(screen.getByRole("button"));
// 记录可用角色
import { logRoles } from "@testing-library/react";
logRoles(container);
// 使用 prettyDOM 选项
screen.debug(undefined, 10000); // 最大长度
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({
// 自定义测试 ID 属性
testIdAttribute: "data-my-test-id",
// 异步超时时间
asyncUtilTimeout: 5000,
// 默认隐藏
defaultHidden: true,
// 抛出建议(调试用)
throwSuggestions: true,
});
// ❌ 不要通过类名/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);
// ❌ 不要将 waitFor 与 findBy 一起使用
await waitFor(() => screen.findByText("x")); // findBy 本身就会等待
// ❌ 不要在 waitFor 回调中断言(除非必要)
await waitFor(() => {
expect(mockFn).toHaveBeenCalled(); // 可以 — 需要等待调用
});
// ✅ 对所有查询使用 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 });
// 精确匹配(默认)
getByText("Hello World");
// 子字符串匹配
getByText("llo Worl", { exact: false });
// 正则表达式
getByText(/hello world/i);
// 自定义函数
getByText((content, element) => {
return element.tagName === "SPAN" && content.startsWith("Hello");
});
| 导入 | 用途 |
|---|---|
render | 将组件渲染到 DOM |
screen | 查询渲染的 DOM |
cleanup | 卸载组件(Jest 中自动执行) |
act | 包装状态更新 |
renderHook | 测试自定义钩子 |
within | 将查询限定到元素内 |
waitFor | 重试直到断言通过 |
configure | 设置全局选项 |
userEvent.setup() | 创建用户事件实例 |
每周安装量
231
代码仓库
GitHub 星标数
10
首次出现
2026年1月26日
安全审计
安装于
github-copilot195
opencode189
codex176
gemini-cli171
cursor153
amp143
| 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 |
Install: npm install --save-dev @testing-library/react @testing-library/dom. Recommended extras: @testing-library/user-event and @testing-library/jest-dom. React 19 requires v16.1.0+.
"The more your tests resemble the way your software is used, the more confidence they can give you."
Avoid testing :
Test instead :
Use queries in this order of preference:
// 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!");
// Images
getByAltText("Company logo");
// Title attribute (less reliable)
getByTitle("Close");
// Only when other queries don't work
getByTestId("custom-element");
| Type | No Match | 1 Match | >1 Match | Async |
|---|---|---|---|---|
getBy... | throw | return | throw | No |
queryBy... | null | return | throw | No |
findBy... | throw | return | throw | Yes |
getAllBy... | throw |
When to use :
getBy* — element existsqueryBy* — element may not exist (assertions like expect(...).not.toBeInTheDocument())findBy* — element appears asynchronouslyimport { 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();
});
Always use @testing-library/user-event over 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);
});
await waitFor(() => {
expect(screen.getByText("Loaded")).toBeInTheDocument();
});
// With options
await waitFor(() => expect(callback).toHaveBeenCalled(), {
timeout: 5000,
interval: 100,
});
// Equivalent to: await waitFor(() => getByText('Loaded'))
const element = await screen.findByText("Loaded");
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
// 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 };
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);
});
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
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 });
// 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");
});
| Import | Usage |
|---|---|
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 |
Weekly Installs
231
Repository
GitHub Stars
10
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot195
opencode189
codex176
gemini-cli171
cursor153
amp143
Vue.js测试最佳实践:Vue 3组件、组合式函数、Pinia与异步测试完整指南
3,700 周安装
| array |
| array |
| No |
queryAllBy... | [] | array | array | No |
findAllBy... | throw | array | array | Yes |
waitFor| Retry until assertion passes |
configure | Set global options |
userEvent.setup() | Create user event instance |