mcpField
A
accelint-react-testing

accelint-react-testing

github.com/gohypergiant测试与质量25279 次安装2 周前更新
代码仓库

关于 accelint-react-testing

accelint-react-testing 是面向 React 开发者的 Testing Library 测试指导技能,适用于编写和审查组件测试。内容强调无障碍查询、贴近真实用户的交互,以及验证可观察行为,并提供分主题参考和审计脚本。

主要功能

  1. 01无障碍优先的查询选择
  2. 02userEvent 交互指导
  3. 03异步查询与等待方式
  4. 04带 Provider 的自定义渲染指导
  5. 05测试反模式审计脚本

适用场景

  1. 01编写以用户行为为中心的 React 测试
  2. 02审查测试中的无障碍问题
  3. 03审计查询和事件处理反模式

安装

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

$npx skills add https://github.com/gohypergiant/agent-skills --skill accelint-react-testing

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

包含的文件

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

AGENTS.md
README.md
assets/
custom-render-template.tsx
output-report-template.md
references/
accessibility-queries.md
anti-patterns.md
async-testing.md
custom-render.md
query-priority.md
query-variants.md
user-events.md
scripts/
check-query-priority.sh
detect-wrapper-queries.sh
find-fire-event.sh

详细介绍

React 测试最佳实践

关于如何使用 Testing Library 编写易于维护、以用户为中心的 React 组件测试的专家指南。重点介绍查询选择、优先考虑无障碍性的测试,以及避免依赖实现细节。

编写 React 测试时绝对不要做的事

  • 绝不要在尝试无障碍查询之前先按测试 ID 查询 - 测试 ID 会绕过无障碍验证:带有 data-testid="submit" 但没有无障碍名称的按钮,在测试中可以正常工作,但屏幕阅读器用户无法使用。测试通过测试 ID 后,你交付的 UI 仍可能无法无障碍使用。查询优先级:getByRole > getByLabelText > getByText > getByTestId。列表中的位置越靠后,你的 UI 可用性就越没有保障。
  • 只要有 userEvent 可用,就绝不要用 fireEvent 模拟用户交互 - fireEvent 只会分发单个 DOM 事件,无法涵盖真实用户触发的事件序列:fireEvent.click() 只会触发一个 click 事件,而真实用户会依次触发 focus → mousedown → mouseup → click。使用 fireEvent 测试正常工作的组件,在用户按常规方式交互时可能会在生产环境中出问题。userEvent.click() 会模拟完整的交互序列,从而发现 fireEvent 遗漏的错误。
  • 绝不要测试实现细节,而不测试用户行为 - 验证“状态变量 X 等于 Y”或“函数 Z 已调用”的测试会导致误报:你把 useState 重构为 useReducer,所有测试都会失败,尽管 UI 的表现完全一样。测试实现细节会妨碍重构,也无法让你确信用户体验正常。测试用户看到和执行的内容(渲染结果、交互结果),而不是组件在内部如何实现。
  • 初始渲染后,绝不要从 container 查询或使用解构出来的查询方法 - const { getByText } = render(<Component />) 会创建过期的查询;状态变化后,解构出来的查询仍会搜索初始 DOM 快照,找不到新渲染的元素。这会导致实际存在的元素也报“找不到元素”错误。始终使用 screen.getByText(),它会自动查询当前 DOM 状态。统一使用 screen 也能让测试更易维护——添加新查询时,无需更新解构内容。
  • 绝不要仅为测试而添加 aria-label 或 role 属性 - 如果你添加 aria-label="submit-button" 或 role="button" 只是为了让测试能找到元素,那就本末倒置了。测试应该验证组件本身已具备无障碍性,而不是为了通过测试才让它具备无障碍性。仅供测试使用的 ARIA 会污染生产代码,并掩盖真正的无障碍问题。应先修正组件的语义化 HTML 和现有 ARIA。
  • 绝不要在没有具体断言的情况下对整个组件树进行快照测试 - 超过 500 行的大型快照会因任何改动(更新 classname、新增 prop、元素顺序变化)而失效,迫使评审者批准他们无法有效评估的差异。如果测试失败时只需“更新快照”,却不知道原因,那么这项测试毫无价值。对关键结构(错误消息、数据表)进行快照测试,其他内容则使用有针对性的断言。
  • 绝不要对返回 promise 的操作使用 waitFor - waitFor(() => expect(element).toBeInTheDocument()) 会反复轮询直至超时,而基于 promise 的 findBy 查询可以一次性解决问题:await screen.findByText('loaded') 会等待元素出现,无需轮询。只有无法使用 findBy 的断言(检查元素是否消失、等待属性变化)才使用 waitFor。
  • 绝不要在 waitFor 回调中执行副作用 - waitFor(() => { fireEvent.click(button); expect(text).toBeInTheDocument(); }) 会在 waitFor 重试时多次点击,导致不可预测的行为。waitFor 用于等待断言,而不是触发操作。应在 waitFor 外执行所有操作,然后仅用 waitFor 执行断言:fireEvent.click(button); await waitFor(() => expect(text).toBeInTheDocument()); 或者更好地使用 await userEvent.click(button); expect(await screen.findByText(text)).toBeInTheDocument();。
  • 绝不要创建未说明 Provider 要求的自定义渲染方法 - 未说明所需 store 结构的自定义 renderWithRedux 函数会给每位开发者带来麻烦:他们调用 render(<Component />),而不是 renderWithRedux(),随后测试因晦涩的“Cannot read property of undefined”错误而失败,浪费 15 分钟排查问题。应在测试工具中集中配置 Provider,并使用 TypeScript 类型确保正确用法,或者醒目地说明所需的包装组件。
  • 绝不要混用不同 Testing Library 导入中的查询方法 - 同时导入 @testing-library/react 的 render 和 @testing-library/dom 的查询方法会造成混乱:react 包中的 screen 无法与 dom 包中的 getByRole 配合使用,导致“screen.getByRole is not a function”错误。对于 React 组件,应从 @testing-library/react 导入所有查询方法——它会重新导出 dom 中的所有内容,并提供针对 React 的增强功能。

编写测试前,先自问

在编写 React 组件测试前,应用以下思考模式:

查询方式选择策略
  • 哪种查询方式符合用户查找此元素的习惯? 真实用户不会查找测试 ID 或 CSS 类,而是查找标签、按钮和标题。如果无法通过角色或标签查询,说明界面缺乏可访问性。查询难度会在问题进入生产环境前揭示用户体验问题。
  • 是否需要查找此元素? 并非每个元素都需要查询断言。用户不会验证“加载旋转图标是否存在”,而是验证“加载完成后是否显示数据”。测试结果,而不是中间状态。
  • 我应该使用 getBy、queryBy 还是 findBy? 对于立即存在的元素,优先使用 getBy,它能提供最好的错误消息。仅在断言元素不存在时(.not.toBeInTheDocument())使用 queryBy。对于异步出现的元素,使用 findBy。切勿使用 queryBy + expect(...).toBeInTheDocument();元素缺失时,改用 getBy 以获得更好的错误消息。
用户测试与实现测试
  • 用户会怎样验证此功能正常? 用户会点击按钮并阅读文本,而不会检查状态变量或模拟函数调用。如果测试使用了 rerender() 或访问了组件内部逻辑,那就是在测试实现。改为通过用户操作进行测试。
  • 如果重构不改变行为,此测试还能通过吗? 如果重命名函数,或从 useState 切换到 useReducer 会导致测试失败,那么你测试的是实现细节。这类测试会阻碍安全变更、浪费时间,却无法让你确信界面确实正常运行。
异步与时序
  • 此查询针对的是异步加载的内容吗? 对于通过 useEffect、API 调用或 setTimeout 加载的内容,使用 findBy*。如果元素不存在,getBy* 会立即抛出错误;findBy* 则会等待元素出现。对异步内容使用 getBy 会产生竞态条件,导致测试只在 CI 中失败。
  • 我是在等待元素出现还是消失? 元素出现 = findBy* 查询。元素消失 = waitForElementToBeRemoved。状态变化 = 使用断言搭配 waitFor。它们的语义各不相同;使用错误的方式会导致测试不稳定或超时变长。
测试隔离与设置
  • 此组件是否需要上下文提供者才能渲染? 使用 useContext、Redux 钩子或 React Router 的组件若缺少提供者就会抛出错误。创建自定义渲染工具,在渲染组件时自动包装所需的提供者。在每个测试文件中重复设置提供者,会让维护变得一团糟。
  • 此测试用例所需的最精简设置是什么? 过度设置的测试(例如,为按钮测试模拟 10 个函数)既脆弱又缓慢。只模拟外部依赖(API、localStorage),不要模拟自己的函数。如果设置很复杂,问题可能出在组件设计上。

使用方法

此技能采用渐进式披露,以尽量减少上下文用量:

1. 从概览开始(AGENTS.md)

阅读 AGENTS.md,了解所有规则的简明概览及其一句话摘要。

2. 按需加载特定规则

使用以下明确的触发条件,了解何时加载各个参考文件:

必须加载(加载整个文件):

遇到以下模式时加载:

除非有明确需要,否则不要加载:

3. 应用模式

每个参考文件都包含:

  • ❌ 展示反模式的错误示例
  • ✅ 展示最佳实现的正确示例
  • 对模式重要性的说明
4. 审查现有测试(可选)

使用提供的脚本审查现有测试套件:

# Check query priority (testId usage, container.querySelector)
./scripts/check-query-priority.sh

# Find fireEvent that should be userEvent
./scripts/find-fire-event.sh

# Detect deprecated wrapper/container patterns
./scripts/detect-wrapper-queries.sh
5. 使用报告模板

调用此技能进行测试代码审查时,请使用标准化的报告格式:

模板: assets/output-report-template.md

报告格式提供:

  • 执行摘要,包含无障碍性置信度和以用户为中心的覆盖率评估
  • 严重级别(Critical、High、Medium、Low),用于确定优先级
  • 影响分析(无障碍性置信度、以用户为中心的置信度、测试可靠性、重构安全性)
  • 分类(查询优先级、查询变体、用户事件、异步测试、自定义渲染、无障碍性、反模式)
  • 模式参考链接,指向 references/ 中的详细指南
  • 用于跟踪所有问题的汇总表

何时使用报告模板:

  • 通过 /accelint-react-testing <path> 直接调用技能
  • 用户要求跨一个或多个文件“审查测试代码”或“审计测试”,从而隐式调用技能

何时不使用报告模板:

  • 用户要求“为此函数编写测试”(直接实现)
  • 用户询问“此测试有什么问题?”(直接回答问题)
  • 用户要求修复特定测试(直接应用修复,不提供正式报告)

此技能涵盖的内容

React Testing Library 模式专家指南:

  1. 查询优先级 - 从 getByRole 到 getByTestId 的无障碍查询层级
  2. 查询变体 - 针对不同场景,何时使用 getBy、findBy、queryBy
  3. 用户事件 - 使用 userEvent 还是 fireEvent 进行逼真的交互测试
  4. 异步测试 - 处理 promise、waitFor、findBy 查询,避免 act 警告
  5. 自定义渲染 - 为复杂组件设置 provider(Context、Redux、Router)
  6. 无障碍查询 - 使用角色、标签和 ARIA 属性进行测试
  7. 反模式 - 避免实现细节、使用 container 和过多快照
  8. 审计脚本 - 自动检测现有测试中的次优模式

查询选择决策树

选择查询时请使用以下层级,从上到下依次尝试:

1. getByRole          ← Preferred: Accessible, reflects how users & ATs interact
   ↓ Can't find role?
   
2. getByLabelText     ← For form fields: matches how users read forms
   ↓ No label?
   
3. getByPlaceholderText  ← For inputs: less accessible than labels
   ↓ No placeholder?
   
4. getByText          ← For non-interactive content: headings, paragraphs
   ↓ Text not unique?
   
5. getByDisplayValue  ← For form inputs: current value
   ↓ No display value?
   
6. getByAltText       ← For images: alt attribute
   ↓ No alt text?
   
7. getByTitle         ← For title attribute: less accessible
   ↓ No title?
   
8. getByTestId        ← Last resort: no accessibility verification

关键原则:

  • 优先级越高的查询,对无障碍性的信心越强
  • 如果无法按角色或标签查询,请先修复组件的无障碍性
  • 使用 getByTestId 就意味着“我已确认这里无法实现无障碍查询”

重要说明

  • screen 导出并非魔法——它只是 getQueriesForElement(document.body)。使用 screen.getByRole() 与从 render 解构出的 getByRole() 完全相同,但重新渲染后 screen 始终有效。
  • Testing Library 通过让易于访问的元素最容易被查询,来鼓励无障碍设计——如果查询很困难,你的 UI 就很难使用。查询困难是 UX 代码异味。
  • 查询失败时使用 screen.debug() 或 screen.logTestingPlaygroundURL()——当 getByRole 失败时,运行 screen.debug() 查看当前 DOM,或运行 screen.logTestingPlaygroundURL() 获取一个交互式工具,查看哪些查询可用。不要猜测选择器——让 Testing Library 显示可用内容。
  • queryBy 会静默返回 null——使用 getBy 可获得更好的错误信息——当元素应该存在时,getBy* 会抛出异常,并提供有关相似元素和可用角色的实用建议。queryBy* 会返回 null,需要你自行添加断言,而且错误输出不够实用。仅在使用 .not.toBeInTheDocument() 断言元素不存在时使用 queryBy。
  • Act 警告表示 React 状态更新发生在 Testing Library 感知范围之外——通常由测试完成后 Promise 才 resolve,或异步查询缺少 await 引起。正确使用 findBy 或 waitFor 不会导致此问题。
  • userEvent 方法是异步的(返回 Promise),fireEvent 方法是同步的——忘记 await userEvent.click() 会导致“act”警告和不稳定的测试,因为状态更新会在断言运行后才发生。

评论

支持 Markdown