Back

How to Query the DOM in React Testing Library

How to Query the DOM in React Testing Library

React Testing Library DOM queries are the foundation of effective component testing, yet many developers struggle with choosing the right query method for their specific testing scenarios. Whether you’re dealing with synchronous elements, async components, or conditional rendering, understanding when to use getBy, findBy, and queryBy methods can make the difference between reliable tests and flaky ones.

This guide covers the essential React Testing Library query methods, their behavioral differences, and practical examples to help you write more robust tests. You’ll learn query priority guidelines, common pitfalls to avoid, and real-world testing patterns that improve test reliability.

Key Takeaways

  • Use getBy queries for elements that should be immediately present after rendering
  • Use findBy queries for elements that appear asynchronously, such as after API calls or state updates
  • Use queryBy queries when testing that elements are not present in the DOM
  • Prioritize getByRole and getByLabelText queries for better accessibility testing
  • Reserve getByTestId for complex scenarios where semantic queries aren’t practical
  • Debug failed queries using screen.debug() and Testing Playground for better query selection

Getting Started with React Testing Library DOM Queries

React Testing Library builds on top of DOM Testing Library by adding APIs specifically designed for testing React components. Install it in your React project:

npm install --save-dev @testing-library/react @testing-library/jest-dom

React Testing Library DOM queries work by finding elements in the rendered component’s DOM tree, similar to how users interact with your application. Here’s a basic example:

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()
})

The key difference from DOM Testing Library is that React Testing Library automatically handles React-specific concerns like component rendering, state updates, and cleanup.

Understanding React Testing Library Query Types

React Testing Library provides three main query types, each with different behaviors for handling missing elements and timing.

getBy Queries - Synchronous Element Selection

getBy queries return elements immediately and throw errors if elements aren’t found or if multiple matches exist:

// Single element - throws if not found or multiple found
const button = screen.getByRole('button', { name: 'Submit' })

// Multiple elements - throws if none found
const listItems = screen.getAllByRole('listitem')

Use getBy queries when you expect elements to be present in the DOM immediately after rendering.

findBy Queries - Asynchronous DOM Queries

findBy queries return promises and retry until elements appear or timeout occurs (default 1000ms):

// Wait for async element to appear
const successMessage = await screen.findByText('Profile updated successfully')

// Multiple async elements
const loadedItems = await screen.findAllByTestId('product-card')

Use findBy queries for elements that appear after API calls, state updates, or other asynchronous operations.

queryBy Queries - Conditional Element Testing

queryBy queries return null when elements aren’t found, making them perfect for testing element absence:

// Test element doesn't exist
const errorMessage = screen.queryByText('Error occurred')
expect(errorMessage).not.toBeInTheDocument()

// Multiple elements - returns empty array if none found
const hiddenElements = screen.queryAllByTestId('hidden-item')
expect(hiddenElements).toHaveLength(0)
Query Type0 Matches1 Match>1 MatchesAsync
getByThrow errorReturn elementThrow errorNo
queryByReturn nullReturn elementThrow errorNo
findByThrow errorReturn elementThrow errorYes

React Testing Library Query Priority Guide

React Testing Library encourages testing components as users interact with them. This priority guide helps you choose queries that reflect real user behavior.

Accessibility-First Queries (getByRole, getByLabelText)

getByRole should be your first choice for most elements:

// Buttons, links, form controls
const submitButton = screen.getByRole('button', { name: 'Create Account' })
const navigationLink = screen.getByRole('link', { name: 'About Us' })

// Headings with specific levels
const pageTitle = screen.getByRole('heading', { level: 1 })

getByLabelText works best for form fields:

const emailInput = screen.getByLabelText('Email Address')
const passwordInput = screen.getByLabelText(/password/i)

These queries ensure your components work with assistive technologies.

Content-Based Queries (getByText, getByPlaceholderText)

getByText finds elements by their visible text content:

// Exact text match
const welcomeMessage = screen.getByText('Welcome back, John!')

// Regex for flexible matching
const errorText = screen.getByText(/something went wrong/i)

getByPlaceholderText helps when labels aren’t available:

const searchInput = screen.getByPlaceholderText('Search products...')

When to Use getByTestId

Reserve getByTestId for cases where semantic queries aren’t sufficient:

// Dynamic content where text changes
const userAvatar = screen.getByTestId('user-avatar')

// Complex components without clear roles
const chartContainer = screen.getByTestId('sales-chart')

Add test IDs sparingly and prefer semantic queries whenever possible.

Common React Testing Library DOM Query Pitfalls

Avoiding Flaky Tests with Proper Query Selection

Problem: Using getBy queries for elements that load asynchronously:

// ❌ Flaky - element might not be loaded yet
test('shows user profile', () => {
  render(<UserProfile userId="123" />)
  const userName = screen.getByText('John Doe') // May fail
})

Solution: Use findBy for async elements:

// ✅ Reliable - waits for element to appear
test('shows user profile', async () => {
  render(<UserProfile userId="123" />)
  const userName = await screen.findByText('John Doe')
  expect(userName).toBeInTheDocument()
})

Debugging Failed Queries

When queries fail, React Testing Library provides helpful debugging tools:

// See what's actually in the DOM
screen.debug()

// Get suggestions for better queries
screen.getByRole('button') // Error message suggests available roles

Use Testing Playground to experiment with queries on your actual HTML.

Over-reliance on Test IDs

Problem: Using getByTestId as the default query method:

// ❌ Not user-focused
const button = screen.getByTestId('submit-button')

Solution: Use semantic queries that reflect user interaction:

// ✅ User-focused
const button = screen.getByRole('button', { name: 'Submit Form' })

Test IDs should be your last resort, not your first choice.

Real-World React Testing Library Examples

Here are practical examples showing different query methods in action:

Form Testing:

import { render, screen, fireEvent } from '@testing-library/react'

test('handles form submission', async () => {
  render(<ContactForm />)
  
  // Use getByLabelText for form fields
  const nameInput = screen.getByLabelText('Full Name')
  const emailInput = screen.getByLabelText('Email')
  const submitButton = screen.getByRole('button', { name: 'Send Message' })
  
  // Fill form and submit
  fireEvent.change(nameInput, { target: { value: 'John Doe' } })
  fireEvent.change(emailInput, { target: { value: 'john@example.com' } })
  fireEvent.click(submitButton)
  
  // Wait for success message
  const successMessage = await screen.findByText('Message sent successfully!')
  expect(successMessage).toBeInTheDocument()
})

Error State Testing:

import { rest } from 'msw'

test('displays error when API fails', async () => {
  // Mock API failure
  server.use(
    rest.get('/api/users', (req, res, ctx) => {
      return res(ctx.status(500))
    })
  )
  
  render(<UserList />)
  
  // Wait for error message to appear
  const errorMessage = await screen.findByText(/failed to load users/i)
  expect(errorMessage).toBeInTheDocument()
  
  // Verify loading state is gone
  const loadingSpinner = screen.queryByTestId('loading-spinner')
  expect(loadingSpinner).not.toBeInTheDocument()
})

Conclusion

Mastering React Testing Library DOM queries requires understanding when to use getBy for immediate elements, findBy for async content, and queryBy for testing element absence. Prioritize accessibility-focused queries like getByRole and getByLabelText, and reserve getByTestId for edge cases where semantic queries aren’t sufficient.

The key to reliable tests is choosing queries that mirror how users interact with your components. This approach creates tests that are both robust and maintainable, catching real user-facing issues while remaining resilient to implementation changes.

FAQs

Use findBy queries when testing elements that appear after asynchronous operations like API calls, setTimeout, or state updates. findBy queries automatically retry until the element appears or the timeout is reached, preventing flaky tests.

getByTestId queries don't reflect how users interact with your application. Users don't see test IDs - they interact with buttons, read text, and use form labels. Semantic queries like getByRole and getByText create more realistic and maintainable tests.

Use queryBy queries, which return null when elements aren't found instead of throwing errors. For example: expect(screen.queryByText('Error message')).not.toBeInTheDocument().

getAllBy queries return arrays of elements immediately and throw errors if no elements are found. findAllBy queries return promises that resolve to arrays and wait for at least one element to appear before resolving.

Use screen.debug() to see the current DOM structure, check the error messages for query suggestions, and try Testing Playground to experiment with different query approaches on your actual HTML.

Listen to your bugs 🧘, with OpenReplay

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