关于 web-testing-react-testing-library
为使用 React Testing Library 的开发者提供组件、Hook 与异步界面测试模式指南。内容强调通过可访问性查询和模拟用户交互验证可见行为,而非实现细节。
主要功能
- 01按可访问性优先级选择查询
- 02使用 userEvent 模拟交互
- 03处理异步元素与断言
- 04用 renderHook 测试 Hook
- 05封装 Provider 并限定查询范围
- 06验证键盘操作与 ARIA 状态
适用场景
- 01测试表单输入和按钮操作
- 02验证加载、错误及成功状态
- 03在应用 Provider 环境中测试组件
- 04检查键盘导航和辅助技术可达性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-testing-react-testing-library安装后在 Claude Code 中输入/web-testing-react-testing-library确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/accessibility.mdasync-testing.mdconfiguration.mdcore.mdcustom-render.mdhooks.mdscoped-queries.mduser-events.md详细介绍
React Testing Library 模式
快速指南: 通过无障碍树进行查询(优先使用
getByRole,最后才用getByTestId),使用userEvent而非fireEvent驱动交互;元素异步出现时,优先使用findBy*。从 v14 起,userEvent采用异步调用并优先使用setup(),因此每次调用都要使用await。该库负责渲染和交互;断言 API、模拟函数和虚假计时器则由执行此文件的测试运行器提供。
详细资源:
- examples/core.md — 查询层级,以及如何在角色、标签、文本和测试 ID 之间进行选择
- examples/user-events.md — 输入、点击、键盘快捷键、Tab 顺序,以及
fireEvent仍适用的情况 - examples/async-testing.md —
findBy*、waitFor、waitForElementToBeRemoved - examples/custom-render.md — 使用应用的 Provider 包装渲染,并为每项测试设置初始状态
- examples/hooks.md —
renderHook、rerender、unmount、包装器,以及会抛出异常的 Hook - examples/accessibility.md — 键盘导航、ARIA 属性、实时区域、焦点陷阱
- examples/scoped-queries.md — 使用
within()查询表格、卡片、区块和模态框 - examples/configuration.md — 虚假计时器、指针事件检查、超时覆盖
- reference.md — 查询和角色表、userEvent 方法、键盘语法、匹配器列表、调试检查清单
适用路径
- 测试对象是组件 — 渲染组件并查询生成的 DOM; 请参阅 examples/core.md 和 examples/user-events.md。
- 测试对象是独立 Hook — 没有可查询的 DOM,因此
renderHook会公开result.current; 请参阅 examples/hooks.md。如果 Hook 只有在其 UI 中才有意义,则应测试组件。 - 测试对象需要 Provider — 主题、身份验证、路由等;使用自定义渲染进行一次包装,而非在每项测试中重复包装; 请参阅 examples/custom-render.md。
<critical_requirements>
编写 React Testing Library 代码前
按可访问性顺序选择查询方式——先用 getByRole,再用 getByLabelText,然后用 getByText,只有在其他方式都无法定位元素时才用 getByTestId。 查询不得不退而求其次,说明辅助技术也无法访问该元素。
在第一次交互前调用 userEvent.setup(),并等待每个方法完成。 从 v14 开始,这些方法会返回 Promise;如果调用时不等待,断言就会针对尚未完成更新的 DOM 运行。
对于异步出现的内容,使用 findBy*。 它会重试,并报告最终放弃时的 DOM;而用 waitFor 包裹 getBy* 时,只会报告最后一次抛出的错误。
通过 screen 查询,而不是使用 render 返回的对象。 一个导入即可用于所有查询;随着测试不断扩展,解构出来的形式容易与实际情况脱节。
断言用户能看到或操作的内容。 内部状态、引用和实例字段会在重构时改变,即使行为保持不变。
</critical_requirements>
自动检测: @testing-library/react、@testing-library/user-event、@testing-library/dom、render、screen、userEvent.setup、fireEvent、waitFor、waitForElementToBeRemoved、findByRole、getByRole、getByLabelText、queryByRole、renderHook、result.current、within、cleanup、prettyDOM、logRoles、logTestingPlaygroundURL、testIdAttribute、asyncUtilTimeout
适用场景:
- 渲染 React 组件,并断言其在 DOM 中呈现的内容
- 通过模拟用户操作来填写表单、进行键盘导航和指针交互
- 测试异步完成的加载、错误和成功状态
- 单独测试自定义 Hook,并判断何时不应这样做
- 构建包含应用程序 Provider 的渲染环境
- 断言交互元素可通过角色、标签和键盘访问
其他内容另行处理:
- 测试运行器机制——文件中的
describe/it、断言 API、模拟函数、虚假计时器和设置文件都由测试运行器负责;本技能只指出需要将选项传给测试运行器计时器推进函数的接口 - 网络测试替身——组件并不知道其传输方式,因此由负责拦截请求的机制来安排响应
- 跨越页面导航并连接真实后端的流程——通过浏览器驱动的端到端测试覆盖
- 渲染的画面外观是否依然正确——与经批准的图像进行像素比较
理念
该库自身的指导原则是:测试越接近软件的实际使用方式,带来的信心就越大。由此可得出三个结论。
查询本质上是一次可访问性审查,只不过恰好采用了测试的形式。 能通过 getByRole("button", { name: /submit/i }) 找到的组件,出于相同原因,也能被屏幕阅读器访问。如果只有 getByTestId 能奏效,那么测试报告的其实是 UI 缺陷,而不是测试本身的问题。
实现细节是指重构后仍保持不变的内容。 状态结构、Hook 内部实现、类名和 DOM 嵌套都可以改变;渲染文本、角色和可访问名称才是约定的一部分。
DOM 是真实的。 组件会渲染到真实文档中,因此组件与其子组件之间的集成错误会在这里暴露,而不是等到生产环境才出现。
核心模式
模式 1:查询优先级层级
根据用户查找元素的方式选择查询,而不是根据标记中哪种方式最方便来选择。
screen.getByRole("button", { name: /sign in/i }); // interactive elements
screen.getByLabelText(/password/i); // form fields, incl. password inputs (no textbox role)
screen.getByText(/welcome back/i); // non-interactive content
screen.getByDisplayValue("draft title"); // a field by its current value
screen.getByAltText(/profile photo/i); // images
screen.getByTestId(`product-${generatedId}`); // last resort: genuinely generated content
完整代码:examples/core.md
模式 2:优先使用 userEvent,而非 fireEvent
fireEvent 会派发单个 DOM 事件。userEvent 则会重现真实交互产生的事件序列——依次触发指针、焦点和键盘事件,而处理器错误往往就出现在这里。
import userEvent from "@testing-library/user-event";
const user = userEvent.setup(); // before the first interaction
await user.type(screen.getByLabelText(/email/i), "[email protected]");
await user.click(screen.getByRole("button", { name: /create account/i }));
只有对于用户不会触发的事件,才使用 fireEvent——例如 resize、scroll,或需要控制属性的合成事件。
模式 3:异步工具
当元素出现时使用 findBy*;仅当等待的对象是断言而非元素时使用 waitFor;当元素消失时使用 waitForElementToBeRemoved。
expect(await screen.findByText(/results/i)).toBeInTheDocument();
await waitFor(() => {
expect(handler).toHaveBeenCalledWith("react"); // one assertion, no side effects
});
await waitForElementToBeRemoved(() => screen.queryByRole("progressbar"));
waitFor 会持续轮询,直到回调不再抛出异常。因此,在其中添加第二个断言,会在报告第一个失败之前付出完整的重试成本;而其中的副作用则会在每次轮询时运行。
完整代码:examples/async-testing.md
模式 4:使用 renderHook 测试 Hook
renderHook 无需组件即可挂载 Hook。每次访问时都会重新读取 result.current,因此应保留访问方式,而非其值。
const { result, rerender, unmount } = renderHook(
({ key }) => useLocalStorage(key, "initial"),
{ initialProps: { key: "draft" } },
);
act(() => {
result.current[1]("updated");
});
expect(result.current[0]).toBe("updated");
如果 Hook 与其 UI 密不可分,优先使用组件测试;对于你发布的 Hook,或包含组件测试会掩盖的边界情况的 Hook,使用 renderHook 才有意义。
完整代码:examples/hooks.md
模式 5:使用自定义 Render 配置 Providers
只需包装一次应用的 Providers 并重新导出,这样每个测试就不必重复嵌套配置。
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">,
) {
return render(ui, { wrapper: AllProviders, ...options });
}
export * from "@testing-library/react";
export { customRender as render }; // shadow the default export
扩展选项类型,使其接受每个测试各自的初始状态(例如认证状态或起始路由),这样测试就能声明其前置条件,而无需额外设置。
完整代码:examples/custom-render.md
模式 6:断言可访问行为
除了查找元素,还可以在无障碍树中断言键盘顺序、展开/选中状态和播报内容。
await user.tab();
expect(screen.getByRole("combobox")).toHaveFocus();
await user.keyboard("{Enter}");
expect(screen.getByRole("button", { name: /section 1/i })).toHaveAttribute(
"aria-expanded",
"true",
);
expect(await screen.findByRole("alert")).toHaveTextContent(
/email is required/i,
);
完整代码:examples/accessibility.md
模式 7:使用 within 限定查询范围
within 会将查询限制在某个容器内,这让重复结构也变得可测试。
const bobRow = screen.getAllByRole("row")[2];
await user.click(within(bobRow).getByRole("button", { name: /edit/i }));
const billing = screen.getByRole("region", { name: /billing address/i });
await user.type(
within(billing).getByLabelText(/street address/i),
"123 Billing St",
);
完整代码:examples/scoped-queries.md
模式 8:userEvent 设置选项
有三个选项会改变行为,而非仅调整行为;这三个选项都在 setup() 中设置。
const user = userEvent.setup({
advanceTimers: advanceTimersBy, // your runner's timer-advance fn; without it, fake timers hang
delay: null, // skip the inter-event wait
pointerEventsCheck: 0, // interact with an element whose CSS sets pointer-events: none
});
完整代码:examples/configuration.md
<red_flags>
危险信号
会在运行时出错:
- 调用
userEvent时没有使用await——断言会针对交互前的 DOM 运行,测试结果取决于时序 - 安装了模拟计时器,却没有将
advanceTimers传给setup()——userEvent的内部延迟永远不会结束,测试会一直挂起,直到运行器超时 - 直接调用
userEvent.click(...),而不是在setup()会话上调用——直接 API 已弃用,也不包含会话的指针或键盘状态 - 对异步出现的元素使用
getBy*——会立即抛出异常,而不是等待 - 在
waitFor回调中执行副作用——回调会在每次轮询时运行,因此副作用会重复触发
容易令人意外的行为:
container.querySelector(".btn-primary")返回null,而不是抛出异常,因此失败会在后续断言处才暴露,而不是在查询处——而且它会根据类名和嵌套结构匹配,重构时这些都可以自由更改;这正是查询层级存在的原因queryBy*返回null,而不是抛出异常;这使它适用于断言元素不存在,却不适用于其他情况findBy*的默认超时时间为 1000ms;要延长超时时间,应在第三个参数中设置,而不是第二个- 每次访问
result.current时都会重新读取,因此将值存入局部变量后,下一次act执行完它就会过时 render、fireEvent和userEvent已经会自动包装在act中;再次包装只是多余操作,还会掩盖真正的 act 警告- 在注册了框架适配器的地方会自动运行清理,因此手动添加
afterEach(cleanup)是多余的 - 在一个
waitFor中放入多个断言,第一次失败暴露前会耗尽完整的重试时间窗口 - 空的
waitFor(() => {})只会等到下一次轮询,不会做其他事情——这不过是披着匹配器外衣的休眠 - 大型 DOM 快照只会产生大量噪声,而不会给出原因;快照应保留给图标或面包屑等小型、稳定的输出
screen.debug()会在 7000 个字符处截断,因此调试表单时,输出常常看起来像是在某个元素中途结束