关于 accelint-react-testing
accelint-react-testing 是面向 React 开发者的 Testing Library 测试指导技能,适用于编写和审查组件测试。内容强调无障碍查询、贴近真实用户的交互,以及验证可观察行为,并提供分主题参考和审计脚本。
主要功能
- 01无障碍优先的查询选择
- 02userEvent 交互指导
- 03异步查询与等待方式
- 04带 Provider 的自定义渲染指导
- 05测试反模式审计脚本
适用场景
- 01编写以用户行为为中心的 React 测试
- 02审查测试中的无障碍问题
- 03审计查询和事件处理反模式
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ gohypergiant/ agent-skills --skill accelint-react-testing安装后在 Claude Code 中输入/accelint-react-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdREADME.mdassets/custom-render-template.tsxoutput-report-template.mdreferences/accessibility-queries.mdanti-patterns.mdasync-testing.mdcustom-render.mdquery-priority.mdquery-variants.mduser-events.mdscripts/check-query-priority.shdetect-wrapper-queries.shfind-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. 按需加载特定规则
使用以下明确的触发条件,了解何时加载各个参考文件:
必须加载(加载整个文件):
- 编写任何查询(getBy、findBy、queryBy*)** → query-priority.md
- 模拟用户交互(点击、输入等) → user-events.md
遇到以下模式时加载:
- 对 getBy、findBy 和 queryBy 感到困惑 → query-variants.md
- 涉及 waitFor、异步查询或 “act” 警告 → async-testing.md
- 使用 Context、Redux、Router 的组件 → custom-render.md
- 测试无障碍性或 ARIA 属性 → accessibility-queries.md
- 大量使用 container、wrapper 或 rerender → anti-patterns.md
- 查询失败或找不到合适的选择器 → query-variants.md,了解 screen.debug() 的用法
除非有明确需要,否则不要加载:
- 对于不使用 provider 的简单组件,不要加载 custom-render.md
- 对于同步测试,不要加载 async-testing.md
- 除非测试 ARIA 或无障碍性问题,否则不要加载 accessibility-queries.md
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 模式专家指南:
- 查询优先级 - 从 getByRole 到 getByTestId 的无障碍查询层级
- 查询变体 - 针对不同场景,何时使用 getBy、findBy、queryBy
- 用户事件 - 使用 userEvent 还是 fireEvent 进行逼真的交互测试
- 异步测试 - 处理 promise、waitFor、findBy 查询,避免 act 警告
- 自定义渲染 - 为复杂组件设置 provider(Context、Redux、Router)
- 无障碍查询 - 使用角色、标签和 ARIA 属性进行测试
- 反模式 - 避免实现细节、使用 container 和过多快照
- 审计脚本 - 自动检测现有测试中的次优模式
查询选择决策树
选择查询时请使用以下层级,从上到下依次尝试:
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