如何在 React Testing Library 中查询 DOM

React Testing Library DOM 查询是有效组件测试的基础,但许多开发者在为特定测试场景选择正确的查询方法时会遇到困难。无论您处理的是同步元素、异步组件还是条件渲染,理解何时使用 getBy、findBy 和 queryBy 方法可以决定可靠测试和不稳定测试之间的差异。
本指南涵盖了 React Testing Library 的基本查询方法、它们的行为差异以及实际示例,帮助您编写更健壮的测试。您将学习查询优先级指导原则、需要避免的常见陷阱,以及提高测试可靠性的实际测试模式。
关键要点
- 对于渲染后应立即出现的元素使用 getBy 查询
- 对于异步出现的元素(如 API 调用或状态更新后)使用 findBy 查询
- 当测试元素不存在于 DOM 中时使用 queryBy 查询
- 优先使用 getByRole 和 getByLabelText 查询进行更好的无障碍性测试
- 仅在语义查询不实用的复杂场景中使用 getByTestId
- 使用 screen.debug() 和 Testing Playground 调试失败的查询以获得更好的查询选择
React Testing Library DOM 查询入门
React Testing Library 基于 DOM Testing Library 构建,添加了专门为测试 React 组件设计的 API。在您的 React 项目中安装它:
npm install --save-dev @testing-library/react @testing-library/jest-dom
React Testing Library DOM 查询通过在渲染组件的 DOM 树中查找元素来工作,类似于用户与您的应用程序交互的方式。这里是一个基本示例:
import { render, screen } from '@testing-library/react'
import LoginForm from './LoginForm'
test('renders login form', () => {
render(<LoginForm />)
const usernameInput = screen.getByLabelText('Username')
expect(usernameInput).toBeInTheDocument()
})
与 DOM Testing Library 的关键区别在于,React Testing Library 自动处理 React 特定的关注点,如组件渲染、状态更新和清理。
理解 React Testing Library 查询类型
React Testing Library 提供三种主要查询类型,每种对于处理缺失元素和时机都有不同的行为。
getBy 查询 - 同步元素选择
getBy 查询立即返回元素,如果找不到元素或存在多个匹配项则抛出错误:
// 单个元素 - 如果未找到或找到多个则抛出错误
const button = screen.getByRole('button', { name: 'Submit' })
// 多个元素 - 如果未找到则抛出错误
const listItems = screen.getAllByRole('listitem')
当您期望元素在渲染后立即出现在 DOM 中时使用 getBy 查询。
findBy 查询 - 异步 DOM 查询
findBy 查询返回 promise 并重试直到元素出现或超时(默认 1000ms):
// 等待异步元素出现
const successMessage = await screen.findByText('Profile updated successfully')
// 多个异步元素
const loadedItems = await screen.findAllByTestId('product-card')
对于在 API 调用、状态更新或其他异步操作后出现的元素使用 findBy 查询。
queryBy 查询 - 条件元素测试
queryBy 查询在找不到元素时返回 null,使其非常适合测试元素不存在:
// 测试元素不存在
const errorMessage = screen.queryByText('Error occurred')
expect(errorMessage).not.toBeInTheDocument()
// 多个元素 - 如果未找到则返回空数组
const hiddenElements = screen.queryAllByTestId('hidden-item')
expect(hiddenElements).toHaveLength(0)
查询类型 | 0 个匹配 | 1 个匹配 | >1 个匹配 | 异步 |
---|---|---|---|---|
getBy | 抛出错误 | 返回元素 | 抛出错误 | 否 |
queryBy | 返回 null | 返回元素 | 抛出错误 | 否 |
findBy | 抛出错误 | 返回元素 | 抛出错误 | 是 |
React Testing Library 查询优先级指南
React Testing Library 鼓励像用户与组件交互一样测试组件。此优先级指南帮助您选择反映真实用户行为的查询。
无障碍性优先查询(getByRole、getByLabelText)
getByRole 应该是大多数元素的首选:
// 按钮、链接、表单控件
const submitButton = screen.getByRole('button', { name: 'Create Account' })
const navigationLink = screen.getByRole('link', { name: 'About Us' })
// 特定级别的标题
const pageTitle = screen.getByRole('heading', { level: 1 })
getByLabelText 最适合表单字段:
const emailInput = screen.getByLabelText('Email Address')
const passwordInput = screen.getByLabelText(/password/i)
这些查询确保您的组件与辅助技术配合使用。
基于内容的查询(getByText、getByPlaceholderText)
getByText 通过可见文本内容查找元素:
// 精确文本匹配
const welcomeMessage = screen.getByText('Welcome back, John!')
// 正则表达式进行灵活匹配
const errorText = screen.getByText(/something went wrong/i)
getByPlaceholderText 在没有标签可用时有帮助:
const searchInput = screen.getByPlaceholderText('Search products...')
何时使用 getByTestId
仅在语义查询不足的情况下使用 getByTestId:
// 文本会变化的动态内容
const userAvatar = screen.getByTestId('user-avatar')
// 没有明确角色的复杂组件
const chartContainer = screen.getByTestId('sales-chart')
谨慎添加测试 ID,尽可能优先使用语义查询。
React Testing Library DOM 查询常见陷阱
通过正确的查询选择避免不稳定的测试
问题:对异步加载的元素使用 getBy 查询:
// ❌ 不稳定 - 元素可能尚未加载
test('shows user profile', () => {
render(<UserProfile userId="123" />)
const userName = screen.getByText('John Doe') // 可能失败
})
解决方案:对异步元素使用 findBy:
// ✅ 可靠 - 等待元素出现
test('shows user profile', async () => {
render(<UserProfile userId="123" />)
const userName = await screen.findByText('John Doe')
expect(userName).toBeInTheDocument()
})
调试失败的查询
当查询失败时,React Testing Library 提供有用的调试工具:
// 查看 DOM 中实际存在的内容
screen.debug()
// 获取更好查询的建议
screen.getByRole('button') // 错误消息建议可用的角色
使用 Testing Playground 在您的实际 HTML 上实验查询。
过度依赖测试 ID
问题:将 getByTestId 作为默认查询方法:
// ❌ 不以用户为中心
const button = screen.getByTestId('submit-button')
解决方案:使用反映用户交互的语义查询:
// ✅ 以用户为中心
const button = screen.getByRole('button', { name: 'Submit Form' })
测试 ID 应该是您的最后选择,而不是首选。
React Testing Library 实际示例
以下是显示不同查询方法实际应用的实用示例:
表单测试:
import { render, screen, fireEvent } from '@testing-library/react'
test('handles form submission', async () => {
render(<ContactForm />)
// 对表单字段使用 getByLabelText
const nameInput = screen.getByLabelText('Full Name')
const emailInput = screen.getByLabelText('Email')
const submitButton = screen.getByRole('button', { name: 'Send Message' })
// 填写表单并提交
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
fireEvent.change(emailInput, { target: { value: 'john@example.com' } })
fireEvent.click(submitButton)
// 等待成功消息
const successMessage = await screen.findByText('Message sent successfully!')
expect(successMessage).toBeInTheDocument()
})
错误状态测试:
import { rest } from 'msw'
test('displays error when API fails', async () => {
// 模拟 API 失败
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.status(500))
})
)
render(<UserList />)
// 等待错误消息出现
const errorMessage = await screen.findByText(/failed to load users/i)
expect(errorMessage).toBeInTheDocument()
// 验证加载状态已消失
const loadingSpinner = screen.queryByTestId('loading-spinner')
expect(loadingSpinner).not.toBeInTheDocument()
})
结论
掌握 React Testing Library DOM 查询需要理解何时对立即元素使用 getBy、对异步内容使用 findBy、对测试元素不存在使用 queryBy。优先使用以无障碍性为中心的查询,如 getByRole 和 getByLabelText,仅在语义查询不足的边缘情况下使用 getByTestId。
可靠测试的关键是选择反映用户如何与组件交互的查询。这种方法创建既健壮又可维护的测试,捕获真实的面向用户的问题,同时对实现变化保持弹性。
常见问题
当测试在异步操作(如 API 调用、setTimeout 或状态更新)后出现的元素时使用 findBy 查询。findBy 查询会自动重试直到元素出现或达到超时,防止不稳定的测试。
getByTestId 查询不反映用户如何与您的应用程序交互。用户看不到测试 ID - 他们与按钮交互、阅读文本并使用表单标签。像 getByRole 和 getByText 这样的语义查询创建更现实和可维护的测试。
使用 queryBy 查询,当找不到元素时返回 null 而不是抛出错误。例如:expect(screen.queryByText('Error message')).not.toBeInTheDocument()。
getAllBy 查询立即返回元素数组,如果未找到元素则抛出错误。findAllBy 查询返回解析为数组的 promise,并在至少一个元素出现后才解析。
使用 screen.debug() 查看当前 DOM 结构,检查错误消息中的查询建议,并尝试使用 Testing Playground 在您的实际 HTML 上实验不同的查询方法。