12k
All articles

如何在 React Testing Library 中查询 DOM

对比 React Testing Library 中的 getBy、findBy 和 queryBy 方法,针对同步、异步及条件性 DOM 元素编写可靠的组件测试。

OpenReplay Team
OpenReplay Team
如何在 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。

可靠测试的关键是选择反映用户如何与组件交互的查询。这种方法创建既健壮又可维护的测试,捕获真实的面向用户的问题,同时对实现变化保持弹性。

常见问题

何时应该使用 findBy 而不是 getBy 查询?

当测试在异步操作(如 API 调用、setTimeout 或状态更新)后出现的元素时使用 findBy 查询。findBy 查询会自动重试直到元素出现或达到超时,防止不稳定的测试。

为什么 React Testing Library 建议避免使用 getByTestId?

getByTestId 查询不反映用户如何与您的应用程序交互。用户看不到测试 ID - 他们与按钮交互、阅读文本并使用表单标签。像 getByRole 和 getByText 这样的语义查询创建更现实和可维护的测试。

如何测试元素未被渲染?

使用 queryBy 查询,当找不到元素时返回 null 而不是抛出错误。例如:expect(screen.queryByText('Error message')).not.toBeInTheDocument()。

getAllBy 和 findAllBy 查询之间有什么区别?

getAllBy 查询立即返回元素数组,如果未找到元素则抛出错误。findAllBy 查询返回解析为数组的 promise,并在至少一个元素出现后才解析。

当我的 React Testing Library 查询失败时如何调试?

使用 screen.debug() 查看当前 DOM 结构,检查错误消息中的查询建议,并尝试使用 Testing Playground 在您的实际 HTML 上实验不同的查询方法。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.