mcpField
R
react-component-testing

react-component-testing

github.com/bm629测试与质量597 次安装1 天前更新
代码仓库

关于 react-component-testing

react-component-testing 是面向 Vite、React 和 TypeScript 项目的组件测试技能,组合 RTL、MSW 与 vitest-axe。它侧重模拟网络边界并验证用户可见行为和运行时可访问性。

主要功能

  1. 01配置 jsdom 组件测试环境
  2. 02用 RTL 查询并渲染组件
  3. 03用 user-event 模拟交互
  4. 04用 MSW 拦截网络请求
  5. 05用 vitest-axe 检查可访问性
  6. 06测试 TanStack Query 状态与表单

适用场景

  1. 01为 React 组件编写交互测试
  2. 02设置 RTL、MSW 与无障碍测试
  3. 03模拟数据请求并验证表单提交
  4. 04覆盖查询的加载、成功和错误状态

安装

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

$npx skills add https://github.com/bm629/agent-skills --skill react-component-testing

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

包含的文件

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

references/
accessibility-with-vitest-axe.md
browser-mode.md
msw-and-generated-clients.md
sources.md
states-and-forms.md

详细介绍

react-component-testing — SKILL.md

**变体:**标准 · **使用时机:**调用此技能后,它会运行至完成,并返回一套已配置并编写好的组件测试层(环境 + setupFiles + 一份完整示例测试),随后控制权交还给调用方。

在 forge 验证的版本。 @testing-library/react 16.x · @testing-library/user-event 14.x · @testing-library/jest-dom 6.x · msw 2.x · vitest-axe 0.1.x(封装 axe-core)· @tanstack/react-query 5.x · vitest 3.x · jsdom。讲解时不依赖特定版本;实际使用时,请根据维护者文档再次确认准确的主版本以及 happy-dom 的注意事项(参见 references/sources.md)。

概述

组件测试是测试金字塔的中间层:位于 Vitest 单元测试(纯逻辑,由 vitest 技能负责运行器)之上,Playwright 端到端(e2e)测试(由 playwright-best-practices 技能负责)之下。组件测试会渲染真实的组件树,并断言用户看到和执行的操作,从而捕捉组件、hooks 与数据获取之间的集成问题;这些问题是单元测试会遗漏的,而这种测试方式也比通过浏览器 e2e 驱动每种状态便宜得多。三种工具共同构成一个紧密协作的测试层:**React Testing Library(RTL)**以用户感知组件树的方式渲染并查询组件树,再通过 user-event 驱动交互;Mock Service Worker(MSW v2)模拟网络边界,使应用真实的 HTTP 客户端和序列化流程端到端运行;vitest-axe断言渲染结果不存在可检测到的无障碍违规。核心原则是:测试越接近软件的实际使用方式,就越能让你确信软件的行为正确——因此应按角色或标签查询,像用户一样交互,在网络层而非模块层进行模拟,并断言可观察到的行为,绝不依赖实现细节。此技能负责 RTL + MSW + vitest-axe 的测试编写层和组件测试环境(setupFiles);它会引用运行器、路由测试框架和静态无障碍检查的相关技能,但不会重复讲解。

何时启用

  • ✅ 为 Vite + React + TS SPA 编写组件测试:渲染 → 交互 → 断言状态 → 断言无障碍。
  • ✅ 配置组件测试环境:jsdom 环境、@vitejs/plugin-react、setupFiles(jest-dom 匹配器、RTL 清理、MSW 服务生命周期、vitest-axe 扩展)。
  • ✅ 在测试中模拟 HTTP/数据获取边界——尤其是使用生成的 @hey-api/openapi-ts(或 openapi-fetch)客户端时——以便测试真实客户端并发现契约偏差。
  • ✅ 通过改变模拟的网络响应,测试 TanStack Query 的加载、空数据、成功和错误状态。
  • ✅ 测试表单:通过 user-event 填写表单,断言 MSW 处理程序捕获的请求正文,并断言 toHaveNoViolations。
  • ✅ 为现有组件测试添加运行时无障碍断言。

请勿在以下情况启用:

  • 任务涉及 Vitest 运行器本身——配置、覆盖率阈值、监视/CI。该任务由 vitest 技能负责;此技能仅作引用。
  • 任务涉及浏览器 e2e 测试——Playwright + @axe-core/playwright 由 playwright-best-practices 负责。(Vitest 浏览器模式作为文档中介绍的替代方案,见 references/browser-mode.md,而非默认方案。)
  • 任务涉及静态无障碍检查——biome 技能负责 lint 阶段的无障碍检查(移植的 jsx-a11y 规则);vitest-axe 是其运行时补充。
  • 任务涉及路由测试框架——渲染路由组件需要 createMemoryHistory + 测试用 RouterProvider,由 tanstack-router 技能负责;此技能仅作引用。
  • 任务涉及 React、TanStack Query 或 shadcn 教程——这些各有对应技能;此技能会与它们配合使用。

工作流

步骤 1:牢记网络边界原则(关键决策)

在网络边界使用 MSW 模拟,而不是在模块边界模拟。 当组件跨越网络时,使用 MSW 拦截 HTTP 请求,让应用真正生成的客户端、查询钩子以及请求/响应序列化逻辑全部运行——这样,契约变更(例如字段重命名或新增必填请求体键)就会在测试中暴露出来。对客户端进行模块模拟(vi.mock('./generated-client'))会返回一个预设对象,并且跳过了正在测试的确切层级,因此永远无法发现契约偏移。

规则: 所有跨越网络的内容都用 MSW 模拟;vi.mock 仅用于非 HTTP 协作者——例如时钟(vi.useFakeTimers)、随机 ID/uuid 生成器或非确定性的浏览器 API。详情及生成客户端拦截的具体说明见 references/msw-and-generated-clients.md。

步骤 2:设置组件测试环境(jsdom,绝不用 happy-dom)

在 vitest.config.ts 中:添加 @vitejs/plugin-react,设置 test.environment: 'jsdom'、test.globals: true(这样 RTL 自动清理和 jest-dom 匹配器可以顺利注册;否则需显式导入 { describe, it, expect, afterEach },并在 afterEach 中自行调用 cleanup()),然后将 test.setupFiles 指向一个设置模块。

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup-tests.ts'],
  },
})
// src/test/setup-tests.ts
import '@testing-library/jest-dom/vitest' // toBeInTheDocument, toHaveValue, ...
import 'vitest-axe/extend-expect'         // toHaveNoViolations
import { afterAll, afterEach, beforeAll } from 'vitest'
import { server } from './msw-server'     // setupServer(...) — see references/msw-and-generated-clients.md

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

运行 vitest-axe 的测试一律使用 jsdom,不要使用 happy-dom。 Happy DOM 有一个有文档记录的 Node.prototype.isConnected 行为,会破坏 axe-core 的遍历,因此 vitest-axe 断言无法在 happy-dom 下正常工作。jsdom 也很快,且对 CI 的负担较小。(请根据当前文档重新确认这一注意事项——参见 references/sources.md;如果确实必须在非无障碍测试中使用 happy-dom,请将 axe 测试放在 jsdom 项目中。)设置 globals: true 后,RTL 会在测试之间自动卸载并清理 DOM;未设置全局变量时,请添加 afterEach(cleanup)。

步骤 3:以用户感知界面的方式渲染和查询

使用 render(<Component />) 和全局对象 screen。按无障碍查询优先级进行查询——越靠前越好:

  1. getByRole(带无障碍名称,例如 screen.getByRole('button', { name: /save/i }))——这是辅助技术和用户感知到的内容;默认首选。
  2. getByLabelText(表单字段),然后是 getByPlaceholderText。
  3. getByText(非交互文案),然后是 getByDisplayValue、getByAltText、getByTitle。
  4. getByTestId——仅作为最后手段,在没有任何面向用户的内容可用于识别节点时使用。

断言行为,而非实现:关注屏幕上显示的内容以及用户可以执行的操作,绝不要关注组件状态、属性名称或内部调用次数。元素不存在时,getBy* 会抛出异常(用于断言“必须存在”);queryBy* 返回 null(用于断言“必须不存在”);findBy* 是异步的(见下一步)。

步骤 4:使用 user-event v14 驱动交互——始终使用 await

每个测试只创建一次会话,使用 userEvent.setup(),然后对每次交互都使用 await——在 v14 中,每次交互都会返回 Promise,漏掉 await 会导致静默的竞争条件。优先使用 user-event,而不是较底层的 fireEvent;fireEvent 只分发单个原始事件,而 user-event 会模拟真实用户触发的完整事件序列(focus、keydown、input、keyup,等等)。

const user = userEvent.setup()
await user.type(screen.getByLabelText(/email/i), '[email protected]')
await user.click(screen.getByRole('button', { name: /submit/i }))
步骤 5:使用 findBy* / waitFor 处理异步输出

触发获取操作的交互之后,请等待结果。优先使用 findBy*——它将 getBy* + waitFor 结合起来,并不断重试,直到元素出现或超时:expect(await screen.findByText(/saved/i)).toBeInTheDocument()。仅当没有可查询的元素时,才使用单独的 waitFor;此时回调中只能放断言——不要在 waitFor 中执行副作用(例如 user-event 或状态变更),因为回调会重复运行。使用 waitForElementToBeRemoved 断言微调器/加载节点消失。

步骤 6:使用自定义 render 提供应用上下文(TanStack Query、主题等)

大多数组件都需要 Provider。编写一个 test-utils 封装,重新导出 RTL,并提供自定义 render,为每次渲染挂载一个全新的 QueryClient,并设置 retry: false(这样错误状态会立即且确定地显示,而不是进行退避重试):

// src/test/test-utils.tsx
import { type ReactElement, type ReactNode } from 'react'
import { render, type RenderOptions } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function makeWrapper() {
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
  return ({ children }: { children: ReactNode }) => (
    <QueryClientProvider client={client}>{children}</QueryClientProvider>
  )
}

export function renderWithProviders(ui: ReactElement, options?: RenderOptions) {
  return render(ui, { wrapper: makeWrapper(), ...options })
}
export * from '@testing-library/react'

每次渲染使用新的客户端(并在 afterEach 中调用 server.resetHandlers())可避免测试间缓存相互污染。对于带路由的组件,将 tanstack-router 技能中的测试 RouterProvider 和 createMemoryHistory 组合到此封装中——该测试框架由该技能维护;不要在此重新推导。每项测试中都应调整 MSW 响应,以覆盖加载、空({ data: [] })、成功和错误(500 / ProblemDetail)状态——请参阅 references/states-and-forms.md。

步骤 7:使用 vitest-axe 断言无障碍性

渲染后(并到达要检查的状态后),对渲染容器运行 axe,并断言没有违规项。可在设置文件中通过 import 'vitest-axe/extend-expect' 注册匹配器(或通过 vitest-axe/matchers 中的 expect.extend(matchers) 注册):

import { axe } from 'vitest-axe'
const { container } = renderWithProviders(<SignupForm />)
expect(await axe(container)).toHaveNoViolations()

vitest-axe 需要真实 DOM,因此要使用 jsdom(步骤 2)。请如实界定范围: 自动化 axe-core 只能检测 WCAG 问题中的一部分(常见说法是大约三分之一到一半),它可以补充人工和辅助技术审查,但绝不能取代这些审查。详情和规则配置见 references/accessibility-with-vitest-axe.md。

步骤 8:完整示例——表单 → user-event → MSW 断言请求正文 → 状态 → 无无障碍违规
// src/features/signup/SignupForm.test.tsx  (colocated *.test.tsx)
import { http, HttpResponse } from 'msw'
import { axe } from 'vitest-axe'
import { renderWithProviders, screen } from '../../test/test-utils'
import { server } from '../../test/msw-server'
import userEvent from '@testing-library/user-event'
import { SignupForm } from './SignupForm'

it('submits the form and shows success', async () => {
  const user = userEvent.setup()
  let captured: unknown

  server.use(
    http.post('/api/users', async ({ request }) => {
      captured = await request.json()                 // assert the REAL request body
      return HttpResponse.json({ id: 'u1' }, { status: 201 })
    }),
  )

  const { container } = renderWithProviders(<SignupForm />)

  await user.type(screen.getByLabelText(/email/i), '[email protected]')
  await user.type(screen.getByLabelText(/^password/i), 's3cret!')
  await user.click(screen.getByRole('button', { name: /create account/i }))

  expect(await screen.findByText(/account created/i)).toBeInTheDocument()
  expect(captured).toEqual({ email: '[email protected]', password: 's3cret!' })
  // security invariant: a field that must never leave the form is absent both places
  expect(screen.queryByLabelText(/internal token/i)).not.toBeInTheDocument()
  expect(captured).not.toHaveProperty('internalToken')

  expect(await axe(container)).toHaveNoViolations()
})

此示例覆盖整个层:真实客户端会序列化请求正文,MSW 在网络边界捕获请求,页面渲染成功状态,并对输出进行无障碍检查。错误/验证变体和安全不变量模式见 references/states-and-forms.md。

步骤 9:约定 + CI

将组件测试文件 *.test.tsx 放在组件旁边。将 test-utils.tsx、setup-tests.ts 和 msw-server.ts 放在 src/test/ 文件夹中。覆盖率、阈值、监听/CI 和运行器配置由 vitest 技能负责——通过该技能的 test 脚本和 CI 门禁运行测试套件;本技能不会重复讲解运行器。

规则

硬性规则(绝不违反):

  • 使用 MSW 在网络边界进行模拟;绝不要对生成的 HTTP 客户端进行模块模拟。 模块模拟会跳过客户端及其序列化过程,也无法发现契约偏差。vi.mock 仅用于非 HTTP 协作者。
  • 每次 user-event 交互都必须 await。 在 v14 中,每次交互都会返回一个 Promise;漏掉 await 会悄然引发竞态,并导致测试不稳定或产生假阳性。
  • 任何 vitest-axe 测试都必须使用 jsdom,不能使用 happy-dom。 Happy DOM 的 isConnected 行为会破坏 axe 遍历;vitest-axe 无法在其上运行。
  • 必须先运行 server.listen(),再创建 HTTP 客户端。 MSW 会在 listen() 时安装拦截器;在此之前捕获的客户端/fetch 会绕过模拟(请参阅参考资料)。
  • 优先使用无障碍查询(getByRole/getByLabelText);getByTestId 仅作为最后手段。测试可观察到的行为,绝不测试实现细节。
  • waitFor 回调中只能放断言——不能有副作用;有合适的查询时,优先使用 findBy*,而不是手动调用 waitFor。
  • 每次渲染都使用全新的 QueryClient 并设置 retry: false,且每项测试都调用 resetHandlers()——不得共享客户端,也不得让缓存或处理器在测试间相互污染。

偏好(可覆盖):

  • 有可查询的元素时,优先使用 findBy*,而不是 waitFor。
  • 每项测试优先使用一次 userEvent.setup(),而不是已弃用的直接调用 userEvent.click(...)。
  • 在 server.listen() 中保留 onUnhandledRequest: 'error',以便未模拟的请求能明确失败,而不是访问真实网络。
  • 应针对实际发布的状态(加载后、错误后)进行无障碍断言,而不只检查初始渲染。

常见问题

  • 在 user-event 调用中漏掉 await。 断言运行前,交互的 Promise 尚未完成;测试可能因此错误地通过,或偶发失败。务必使用 await。
  • happy-dom + axe = 静默故障。 测试虽然“运行”了,但由于 Node.prototype.isConnected 的 bug,vitest-axe 的行为不正确。请将无障碍测试固定为使用 jsdom。
  • 在调用 listen() 之前创建客户端。 在模块加载时实例化 fetch 的生成客户端(顶层的 const client = createClient())可能会绑定到 MSW 尚未修补的 fetch 引用。确保 MSW 的设置文件位于 setupFiles 中(最先运行);对于某些生成的 /openapi-fetch 客户端,还要显式传入 MSW 可见的 fetch——请参阅 references/msw-and-generated-clients.md。
  • 在处理程序路径中加入查询参数。 MSW 按路径名匹配;在 http.get('/api/x?foo=bar', ...) 中加入 ?foo=bar 不会匹配——应在处理程序中通过 new URL(request.url) 读取查询参数。
  • 在 waitFor 中执行副作用。 waitFor 会多次重新运行回调;在其中调用 user-event 或修改状态会导致重复触发。回调中只保留断言。
  • 忘记 jest-dom 的 /vitest 入口。 在 Vitest 下,导入 @testing-library/jest-dom(Jest 入口)而不是 @testing-library/jest-dom/vitest,会导致匹配器未注册。
  • 在测试之间复用过期的 QueryClient。 复用同一个客户端会缓存第一次响应;后续测试会读取过期数据。每次渲染都创建新客户端。
  • 过度依赖 axe。 零违规是必要条件,但并非充分条件——它只覆盖 WCAG 的部分要求;仍需人工或辅助技术审查。

反模式

  • “直接用 vi.mock 模拟 API 客户端,更简单。” 这样会删掉正在测试的那一层;生成的客户端和序列化逻辑都不会运行,因此无法发现契约漂移。请使用 MSW 模拟网络。
  • “到处用 getByTestId,这样更稳定。” 测试 ID 会绕过无障碍树,让无法访问的 UI 也能通过测试。应按角色或标签查询;testid 是最后的手段。
  • “fireEvent.click 没问题。” 它只派发一个合成事件,而不是真实的交互序列;对于用户实际会执行的操作,应优先使用 user-event。
  • “happy-dom 更快,无障碍测试也用它吧。” 它会导致 axe 出错。速度上的收益不值得换来无声失效的无障碍断言。
  • “断言组件调用了 fetch / 钩子的内部状态。” 这会让测试与实现耦合——重构时,即便行为未变,测试也会失败。应断言用户看到的内容以及服务端收到的请求。
  • “在这里重新讲解 Vitest 配置 / 覆盖率。” 这不在范围内——运行器由 vitest 技能负责;请参考该技能。

输出

为 Vite + React + TS SPA 搭建可正常工作的组件测试层:使用 @vitejs/plugin-react 的 jsdom Vitest 环境;一个 setupFiles 模块,负责配置 jest-dom 匹配器、RTL 清理、MSW 服务生命周期以及 vitest-axe 扩展;一个 test-utils 自定义 render(使用全新的 TanStack Query 客户端、retry:false,并可与路由测试框架组合);以及编写的 *.test.tsx 文件,用于渲染、通过 user-event 驱动交互、使用 MSW 模拟网络(包括生成的客户端)、断言请求正文和 UI 状态,并断言 toHaveNoViolations。使用者是下一阶段工作流中的开发者或被派发的 Agent,他们现在将编写由契约驱动且无障碍的组件测试;运行器、覆盖率和 CI 由 vitest 技能提供。

相关内容

  • vitest — 负责测试运行器、配置、覆盖率阈值、监视和 CI;本技能负责在此基础上编写组件测试环境以及 RTL/MSW/axe 测试。
  • tanstack-query — 提供受测的数据获取钩子;本技能介绍每个测试使用的 QueryClientProvider(retry:false),而非该库本身。
  • tanstack-router — 负责路由器测试框架(createMemoryHistory + 测试用 RouterProvider);对于带路由的组件,将其组合到自定义 render 中。
  • playwright-best-practices — 对应的端到端测试技能(真实浏览器、@axe-core/playwright);组件测试位于其下层。另请参阅 references/browser-mode.md。
  • biome — 静态 jsx-a11y lint(lint 阶段);vitest-axe 是其运行时补充。
  • shadcn — 受测组件和表单。

渐进式披露

  • references/msw-and-generated-clients.md — MSW v2 的 setupServer/http/HttpResponse、Node 生命周期、每个测试的覆盖项、读取请求正文、查询参数警告,以及拦截生成的 @hey-api/openapi-ts / openapi-fetch 客户端(先监听再创建的顺序 + 自定义 fetch 注意事项)。在模拟网络时加载。
  • references/accessibility-with-vitest-axe.md — vitest-axe 设置变体、toHaveNoViolations、jsdom 要求、规则配置,以及如实说明 WCAG 子集的覆盖范围。在添加无障碍断言时加载。
  • references/states-and-forms.md — 使用 MSW + TanStack Query 测试加载/空/成功/错误状态,以及表单 + 请求正文 + 安全不变量断言模式。在测试数据状态或表单时加载。
  • references/browser-mode.md — 文档记录的替代方案:Vitest 浏览器模式(Playwright 提供程序)+ @axe-core/playwright,以及相应权衡(速度较慢、CI 开销更大、无障碍测试从 vitest-axe 转移、与端到端测试技能重叠)。默认使用 jsdom;仅在需要真实浏览器保真度时加载。
  • references/sources.md — 研究来源 + 根据文档核验的清单。

正文预算

  • description ≤ 1,024 个字符。
  • 正文 ≤ 约 500 行 / 5,000 个 token;大量内容放在 references/ 中,按需加载。

评论

支持 Markdown