React Testing Library

Many modern project scaffolds come with React Testing Library pre-configured (Create React App, now deprecated, did this out of the box), or it can easily be added to any setup, such as Vite, with a couple of dependencies. Rather than dealing with instances of rendered React components, React Testing Library works with actual DOM nodes, resembling the way your software is actually used.


// fetch.js
import {useState, useReducer} from 'react'
import axios from 'axios'

const initialState = {
  error: null,
  greeting: null,
}

function greetingReducer(state, action) {
  switch (action.type) {
    case 'SUCCESS': {
      return {
        error: null,
        greeting: action.greeting,
      }
    }
    case 'ERROR': {
      return {
        error: action.error,
        greeting: null,
      }
    }
    default: {
      return state
    }
  }
}

export default function Fetch({url}) {
  const [{error, greeting}, dispatch] = useReducer(
    greetingReducer,
    initialState,
  )
  const [buttonClicked, setButtonClicked] = useState(false)
  const fetchGreeting = async url =>
    axios
      .get(url)
      .then(response => {
        const {data} = response
        const {greeting} = data
        dispatch({type: 'SUCCESS', greeting})
        setButtonClicked(true)
      })
      .catch(error => {
        dispatch({type: 'ERROR', error})
      })
  const buttonText = buttonClicked ? 'Ok' : 'Load Greeting'
  return (
    <div>
      <button onClick={() => fetchGreeting(url)} disabled={buttonClicked}>
        {buttonText}
      </button>
      {greeting && <h1>{greeting}</h1>}
      {error && <p role="alert">Oops, failed to fetch!</p>}
    </div>
  )
}

// __tests__/fetch.test.js
import {http, HttpResponse} from 'msw'
import {setupServer} from 'msw/node'
import {render, fireEvent, waitFor, screen} from '@testing-library/react'
import '@testing-library/jest-dom'
import Fetch from '../fetch'

const server = setupServer(
  http.get('/greeting', () => {
    return HttpResponse.json({greeting: 'hello there'})
  }),
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

test('loads and displays greeting', async () => {
  render(<Fetch url="/greeting" />)
  fireEvent.click(screen.getByText('Load Greeting'))
  await waitFor(() => screen.getByRole('heading'))
  expect(screen.getByRole('heading')).toHaveTextContent('hello there')
  expect(screen.getByRole('button')).toBeDisabled()
})

test('handles server error', async () => {
  server.use(
    http.get('/greeting', () => {
      return new HttpResponse(null, {status: 500})
    }),
  )
  render(<Fetch url="/greeting" />)
  fireEvent.click(screen.getByText('Load Greeting'))
  await waitFor(() => screen.getByRole('alert'))
  expect(screen.getByRole('alert')).toHaveTextContent('Oops, failed to fetch!')
  expect(screen.getByRole('button')).not.toBeDisabled()
})

Other APIs: RTL's queries all follow the pattern {get|find|query}[All]By{Role|LabelText|PlaceholderText|Text|DisplayValue|AltText|Title|TestId} — for example, getByRole, findAllByText, queryByTestId, and so on.

Return behavior of React Testing Library's get/find/query variants
No Match 1 Match 1+ Match Await?
getBy Throw Return Throw No
findBy Throw Return Throw Yes
queryBy Null Return Throw No
getAllBy Throw Array Array No
findAllBy Throw Array Array Yes
queryAllBy [] Array array No

// Matching a string:
getByText('Hello World') // full string match
getByText('llo Worl', {exact: false}) // substring match
getByText('hello world', {exact: false}) // ignore case

// Matching a regex:
getByText(/World/) // substring match
getByText(/world/i) // substring match, ignore case
getByText(/^hello world$/i) // full string match, ignore case
getByText(/Hello W?oRlD/i) // advanced regex

// Matching with a custom function:
getByText((content, element) => content.startsWith('Hello'))

waitForElementToBeRemoved (Promise) retries the function until it no longer returns a DOM node.

act is a wrapper around the act testing helper (exported from react-dom/test-utils historically, and from the react package itself as of React 19). React Testing Library wraps render and fireEvent in a call to act already so most cases should not require using it manually.

within takes a node and returns an object with all the queries bound to the node (used to return the queries from React Testing Library's render method). Eg.: within(node).getByText("hello")

configure changes global options. Eg.: configure({testIdAttribute: 'my-data-test-id'})

cleanup clears the DOM; it's used with afterEach to reset the DOM between tests. As of React Testing Library v9, this happens automatically after each test as long as your test framework exposes a global afterEach (both Jest and Vitest do), so manual afterEach(cleanup) calls are rarely needed anymore.

render renders into a container which is appended to document.body. (options: container, baseElement, hydrate, wrapper, queries)


import {render} from '@testing-library/react'
import '@testing-library/jest-dom'

test('renders a message', () => {
  const {container, getByText} = render(<Greeting />)
  expect(getByText('Hello, World!')).toBeInTheDocument()
  expect(container.firstChild).toMatchInlineSnapshot(`
    <h1>Hello, World!</h1>
  `)
})

// Example, a function to traverse table contents
import * as tableQueries from 'my-table-query-library'
import {queries} from '@testing-library/react'

const {getByRowColumn, getByText} = render(<MyTable />, {
  queries: {...queries, ...tableQueries},
})

render Result: ...queries, container, baseElement, debug, rerender, unmount, asFragment


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

const {container, unmount} = render(<Login />)
unmount()
// your component has been unmounted and now: container.innerHTML === ''