Skip to content

React Testing Library Basics, Best Practices, and Guidelines

Austin Sullivan edited this page Aug 11, 2022 · 10 revisions

Philosophy

Read the RTL (React Testing Library) docs intro page for the quick “why” behind RTL. “The RTL way” of testing is primarily behavior-driven, with a focus on not testing implementation details of a component.

Basics

Rendering components

RTL’s rendermethod is used to render components for testing purposes. Much like Enzymes 'mount` method it renders not only the direct component under test, but all children of that component as well.

RTL does not have any shallow rendering functionality, because of its focus on testing as the user would use an application. If you need to effectively shallow render something you will have to mock the child components, see the section on mocking below for more information on this.

Accessing DOM elements

Rather than assigning the value of render to a variable like we would with shallow or mount from Enzyme; we can instead call queries (and some other functions) on RTL’s screen object.

Using screen simplifies test writing because it allows you to use render in one way rather than having to destructure it differently depending on what you want to test.

The exception to using screen is that snapshot testing does require destructuring renders return value to access its asFragment property.

Testing user events

Simulating user input

In RTL documentation you will see mentions of both userEvent and fireEvent,userEvent is the simulation library recommended by the RTL team as its simulations are designed to be more advanced/realistic than the simulations provided by fireEvent. See the userEvent API docs for more information.

Using Act()

Calling act() is typically not needed in RTL, because RTL uses it behind the scenes by default, including when using the userEvent API. You may occasionally get a warning in your terminal stating "an update was not wrapped in act(...)" when you run your tests, the most likely cause is an async operation updating after completion of the test. Some recommended things you can try to solve this error (in order of preference) include:

  1. Use a find query rather than a get query.
  2. If a find query can’t be used the waitFor method is the next item that should be attempted.
  3. If neither of the above resolves the warning, the operation causing the issue should be mocked.
  4. If the operation causing the issue can’t be mocked out you can wrap the action in act().

Note: The use of Jest's fake timer functionality is an exception to the above guideline, if time passing causes state updates you should wrap your advanceTimersByTime calls in act().

Queries

Query types:

  • getBy / getAllBy should be the queries you reach for by default.
  • queryBy / queryByAll should only be used to assert that something is not present, as in that situation getBy will throw an error.
  • findBy / findAllBy should be used when the item you’re querying for may not be immediately available. Note that these will return a promise rather than the element/elements queried.

Priority list

RTL maintains an explicit query priority list for what queries you should attempt to use over others getByRole should be the default query that you use in most situations. Its name option allows you to somewhat combine getByRole with getByText, enabling you to select the vast majority of things you may need. The name option follows the format getByRole(‘heading’, { name: ‘Text visible in the component’ })

Assertions

The custom matchers provided by jest-dom (which come from the @testing-library/jest-dom package) should generally be used for DOM-based assertions rather than defaulting to the more simplistic matchers provided by default in Jest.

Appropriate/more specific matchers should be leveraged as much as possible, for example it’s encouraged to use expect(button).toBeDisabled() vs expect(button.disabled).toBe(true).

Mocking

Because RTL doesn’t support shallow rendering some components may need to be mocked for a pure unit testing approach. RTL itself doesn’t add mocking capabilities, but the mocking basics are being covered here because we may need to do so to replace some tests that previously used shallow.

Callback mocking example

For the component:

const MyButton = ({ onClick }) => <button onClick={onClick}>Text</button>;

Adequate testing for this component’s functionality would ensure that the provided onClick callback prop is called when expected, and also that it’s not called when not expected. It may look like:

describe('functionality', () => {
  it('calls its onClick callback when clicked', () => {
    // define the mock function
    const onClickMock = jest.fn();

    // render the component with our mock
    render(<MyButton onClick={onClickMock}>Text</MyButton>);

    // find the button and click it using userEvent
    userEvent.click(screen.getByRole('button', { name: 'Text' }));

    // assert that it was called as many times as we expect
    expect(onClickMock).toHaveBeenCalledTimes(1);
  });

  it('does not call its onClick callback when not clicked', () => {
    const onClickMock = jest.fn();

    render(<MyButton onClick={onClickMock}>Text</MyButton>);

    // assert that it wasn't called because in this test we didn't click the button
    // this prevents false-positive calls in addition to the false negatives prevented by the first test
    expect(onClickMock).not.toHaveBeenCalled();
  });
});

Child component mocking example

For the component:

import { RandomHeader } from './RandomHeader';

export const MyPage = () => (
  <div>
    <RandomHeader />
    Body text
  </div>
);

Where <RandomHeader /> is a child that returns a header with random text, some testing approaches (such as snapshots) will be impossible without mocking. In order to test this component we can do:

jest.mock('../RandomHeader', () => () => <h1>Header text</h1>);

describe('rendering', () => {
  it('matches the snapshot', () => {
    const { asFragment } render(<MyPage />);

    expect(asFragment()).toMatchSnapshot();
  });
});

Notice that here jest.mock takes two arguments, the path to the component you’re mocking from the test file, and a function that returns another function that returns the JSX being used in your mock.

Mocking a child component with props

If you need a mocked child component to take children or other props which you can test against you can use the following alternative mock setup to do so:

jest.mock('../Header', () => ({
  Header: ({ children, ...props }) => <h1 {...props}>{children}</h1>
});

Testing components that rely on contexts

When the component you’re testing relies on a context, you can either import the actual context that will be used or mock the context similarly to how a child component is mocked.

Whichever way you choose you will need to provide a value prop to the Context.Provider with the needed values, with your component under test as a child. This is done in the same way you would provide the values to the context in real-world usage.

Testing style standards / guidance

Snapshot usage

Snapshots should not be used to assert that a specific class is applied to a component. Tests should explicitly check for the class names that we expect to exist (for instance by using jest-dom's toHaveClass matcher).

Snapshots should be used in situations where a component's structure is the primary concern of the test, i.e. for testing that elements are in the correct order.

Test abstraction vs repeating setup

Whether to abstract elements of test setup from each test block or leave all of a tests setup in the associated test block is a highly variable decision with no hard rules.

Our current guidance is to default to not abstracting setup unless that setup "excessively" bloats the test suite, or the abstraction helps clarify the purpose of the test by making it easier to distinguish what elements of the setup are changing for each test.

Mocking child components vs including them in tests

Current guidance is to default to mocking child components for unit testing of the props a component is passing.

Monolithic vs split test files

Rather than having a single monolithic test file for each component as they appear in our component list, a separate test file should be used for each individual component that we export.

Test nesting and describe() block usage

Test cases do not need to all be wrapped in describe() blocks which only state the component they're for. describe() blocks should be used to group test cases together within a test suite when needed, i.e. when using a shared setup.

it() vs test()

Use test() for your test cases rather than it() when not inside of a describe() block.

Further reading