Some good practices in component testing (using React Testing Library)

Testing approach

  • Use React Testing Library, it’s awesome!
  • Use screen.logTestingPlaygroundURL() for the most semantic and accessible selectors in your tests
  • Use screen.getByRole or screen.getByText primarily for querying elements
  • Use data-testid for querying elements only if getByRole or getByText is not sufficient (e.g. dynamic language changes the label of elements)
  • Don’t use ids or className to find elements
  • Understand the difference between getBy (sync), findBy (async) and queryBy (conditional) element queries
  • Test if component renders with required props
  • Test if component renders with custom props
  • Test component listening to callbacks from child
  • Mock what you can
  • Use easily readable string values in props and assertions instead of object dictionaries
  • Stick to testing boundaries
  • UI components should have an optional testID prop, which renders a data-testid with the appropriate value on the component’s wrapper element
  • Components nested within other components are passed the testID, so they can generate their own internal testIDs (e.g.: ‘hero’ ⇒ ‘hero-image’)
  • Minimise the reliance on data-testid, (but component should support data-testid, so QA testers can use them with e2e tests such as Playwright)
  • Test component states (open modal, close modal)
  • Test with component states with keyboard navigation (TAB, ENTER, ESC)
  • Use userEvent, don’t use fireEvent to fire user events
  • Avoid common mistakes with React Testing Library

The Testing Playground Chrome extension providing an accessible element selector by the user clicking the element in the browser The Testing Playground Chrome extension providing an accessible element selector by the user clicking the element in the browser

Testing recipes

Recipe #1 - Use getByRole and within

A Status banner component used to let users know when their requested action was executed successfully

const letterStatusBannerRegion: HTMLElement = screen.getByRole('region', {
  name: /document request sent via letter/i,
});

expect(letterStatusBannerRegion).toBeInTheDocument();
expect(within(letterStatusBannerRegion).getByText(/by your\.name@email\.com \./i)).toBeInTheDocument();
expect(within(letterStatusBannerRegion).getByText(/at 10:18 on 31 jan 2023./i)).toBeInTheDocument();

Recipe #2 - Testing state in hooks

Interestingly I’ve found that testing state in hooks doesn’t work when using destructuring assignment, because the destructuring assignment returns a “snapshot” of values from the result.current object, and will not update when a state update happens.

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

describe('useCommunication()', () => {
  it('sets the channel', async () => {
    const { result } = renderHook(() =>
      useCommunication({
        channel: 'email',
      }),
    )

    // This will pass
    expect(result.current.channel).toBe('email')
    act(() => {
      result.current.setChannel('letter')
    })
    expect(result.current.channel).toBe('letter')

    // This will fail
    /* 
		const { channel, setChannel } = result.current;
    expect(channel).toBe('email');
    act(() => {
      setChannel('letter');
    });
    expect(channel).toBe('letter');
		*/
  })
})

Recipe #3 - Find an element by role and matching aria description

<p id="message-hint">For example, "Hello world".</p>
<textarea name="message" label="Message" aria-describedby="message-hint" />

DOM of text area described by a paragraph of text

expect(
  screen.getByRole('textbox', {
    description: /for example, "hello world"\./i,
  }),
).toBeVisible()

RTL query to find texture by role and description

Resources