Back

如何在 React Testing Library 中查询 DOM

如何在 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 上实验不同的查询方法。

Listen to your bugs 🧘, with OpenReplay

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