mcpField
W
web-testing-react-testing-library

web-testing-react-testing-library

github.com/agents-inc测试与质量2668 次安装2 天前更新
代码仓库

关于 web-testing-react-testing-library

为使用 React Testing Library 的开发者提供组件、Hook 与异步界面测试模式指南。内容强调通过可访问性查询和模拟用户交互验证可见行为,而非实现细节。

主要功能

  1. 01按可访问性优先级选择查询
  2. 02使用 userEvent 模拟交互
  3. 03处理异步元素与断言
  4. 04用 renderHook 测试 Hook
  5. 05封装 Provider 并限定查询范围
  6. 06验证键盘操作与 ARIA 状态

适用场景

  1. 01测试表单输入和按钮操作
  2. 02验证加载、错误及成功状态
  3. 03在应用 Provider 环境中测试组件
  4. 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.yaml
reference.md
examples/
accessibility.md
async-testing.md
configuration.md
core.md
custom-render.md
hooks.md
scoped-queries.md
user-events.md

详细介绍

React Testing Library 模式

快速指南: 通过无障碍树进行查询(优先使用 getByRole,最后才用 getByTestId),使用 userEvent 而非 fireEvent 驱动交互;元素异步出现时,优先使用 findBy*。从 v14 起,userEvent 采用异步调用并优先使用 setup(),因此每次调用都要使用 await。该库负责渲染和交互;断言 API、模拟函数和虚假计时器则由执行此文件的测试运行器提供。

详细资源:


适用路径

  • 测试对象是组件 — 渲染组件并查询生成的 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,或需要控制属性的合成事件。

完整代码:examples/user-events.md

模式 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 个字符处截断,因此调试表单时,输出常常看起来像是在某个元素中途结束

评论

支持 Markdown