React Testing Library Cheat Sheet

When it comes to testing React apps manually, we can either choose to render individual component trees in a simplified test environment or run the complete app in a realistic browser environment (end-to-end testing). But if we were to do some automated tests, the React team recommends choosing some libraries, one of them being the React Testing Library. But what is it? This React Testing Library Cheat Sheet will answer all your questions!

React Testing Library is a set of helpers built on top of the DOM Testing Library by adding APIs to test React components without relying on their implementation details.

As you can get by its name, this library is specifically built to work with React components, and it has out-of-the-box support if you are using the Create React App toolkit to develop React application.

In this cheat sheet, we will walk you through a majority of its topics suited for beginners to pros. Let's begin!


Basic level - React Testing Library Cheat Sheet


1. What problem is React Testing Library trying to solve, and how does it provide the solution?

The typical problem we see while writing tests for our code is that we need maintainability. Our tests should be maintainable for React components, which involves avoiding our components' implementation details and focusing on making them robust. The testbase should be maintainable in the long run so that the refactoring doesn't break your tests and cause another issue to slow down the development process.

That's what React Testing Library comes in to solve this problem. It's quite a lightweight solution for testing React components providing utility functions on top of react-dom and react-dom/test-utils. The best part is that it allows for these functions in a way that encourages better testing practices.

React Testing Library's primary guiding principle is:

The more your tests resemble the way your software is used, the more confidence they can give you.

Here are some of the ways it provides a better testing solution:

    Your tests work in actual DOM nodes instead of dealing with the instances of rendered React components.
  1. The utilities facilitate querying the DOM the same way the user would without the fluff.
  2. It exposes a recommended way to find elements by a `data-testid` as an 'escape-hatch' for elements where the text content and label don't make sense or aren't practical.
  3. It encourages your applications to be more accessible and makes sure that the app works when a real user uses it.

2. A basic component render test

Let's say we have the following component in our App.js file:

const title = 'Hello, World!';

function App() {
  return <div>{title}</div>;
}

export default App;
```

We can have the following test code in the App.test.js file:

```
import { render } from '@testing-library/react';
import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);
  });
});

React Testing Library's render function takes any JSX code to render it. By this, you have access to the React component in your test. To verify this, we can use the library's debug function like this:

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

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    screen.debug();
  });
});

Now in the command line, you should see the HTML output of the App component as follows:

<body>
  <div>
    <div>
      Hello, World!
    </div>
  </div>
</body>

3. Why use React Testing Library?

When you get to know about React Testing Library, you might find its resemblance to another popular library, Enzyme. Enzyme is a JavaScript Testing utility for React that makes testing your React Component's output easier.

But React Testing Library differs from Enzyme and other testing libraries due to the following points:

1. The primary purpose of this library is to increase confidence in your tests by testing your components in a way the user would use them in real life. So, instead of accessing the component's internal APIs or evaluating their state, React Testing Library gives us more confidence by writing the tests based on the component output.

2. It aims to solve the problems many developers were facing when writing tests using Enzyme, thereby leading them to write implementation details. As a result, the tests slow down development speed and productivity. With React Testing Library, you don't have to worry about rewriting parts of your tests.

3. It increases developer experience in writing tests as the syntax is very intuitive, and you don't have to know any intricacies in order to get up and running with your tests. You can start querying the DOM using methods like getByText, getByAltText etc., like an actual end-user.


4. Queries in React Testing Library

Queries are the methods that the Testing Library gives you to find elements on the page. Some different types of queries you get are "get", "find" and "query" and they differ from each other on the basis of whether the query will throw an error if no element is found or if it will return a Promise and retry.

After the element is selected, you can use the Events API or user-event to fire events and simulate user interactions or even use Jest and jest-dom to make assertions about the element.

Here's an example of a query in React Testing Library:

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

test('should show login form', () => {
 render(<Login />)
 const input = screen.getByLabelText('Username')
 // Events and assertions...
})

Some of the different types of queries we get out of the box can be grouped into single and multiple elements queries.

Single element queries:

  1. getBy...: it returns a matching node for a query and throws a descriptive error if no elements match.
  2. queryBy...: it returns the matching node for a query and returns nil if no elements match. It's used to assert an element that's not present.
  3. findBy...: it returns a Promise which resolves when an element is found that matches the given query. The Promise is rejected if no element is found or more than one element is found.

Multiple elements queries:

  1. getAllBy...: it returns an array of all matching nodes for a query and throws an error if no elements match.
  2. queryAllBy...: it returns an array of all matching nodes for a query and an empty array ([]) if no elements match.
  3. findAllBy...: it returns a promise which resolves an array of elements when any elements are found that match the given query. If no elements are found, the Promise is rejected.

5. What levels of component tree to test?

You might have doubts about whether you should test the children, parents, or both of them in the component tree. But it is helpful to break down how tests are organized around how the user experiences and interacts with the app's functionality rather than around specific components themselves.

Sometimes, it might be helpful to test for and test each reusable component individually; other times, the specific breakdown of a component tree is just an implementation detail, and testing every component individually can cause issues.

Hence it's often preferable to test high enough up the component tree to simulate realistic user interactions.


Intermediate level - React Testing Library Cheat Sheet


1. Comparing React Testing Library with Jest

First and foremost, React Testing Library is not an alternative to Jest. React Testing Library needs Jest (and vice versa), each of which has a predetermined straightforward task.

Jest is the most popular testing framework for JavaScript applications. Apart from being a test runner (i.e., you can run it with npm test once you've set up the test script in your package.json file), Jest offers you the describe-test block and related functions:

describe('my function or component', () => {
  test('does the following', () => {
    // Your code
  });
});

Here the describe block is the test suite, and the test block is the test case. A test suite can have multiple test cases. What you put into the test cases are called assertions, with can either be true or false. Here's an example in Jest:

describe('true is truthy and false is falsy', () => {
  test('true is truthy', () => {
    expect(true).toBe(true);
  });

  test('false is falsy', () => {
    expect(false).toBe(false);
  });
});

As you can see, there's nothing about React components yet in Jest test cases. Hence, we call Jest a test runner. It gives you the ability to run the tests with Jest from the command line. Jest also offers functions for test suites, test cases, or assertions. Hence, we need Jest to do all of that work.

Meanwhile, React Testing Library is one of the testing libraries to test React components. It provides virtual DOMs and isn't specific to any testing framework.


2. Breakdown of a test

Let's say we have the following test file that uses Mock Service Worker; the recommended way to declaratively mock API communication in your tests:

// fetch.test.jsx
import React from 'react'

import {rest} 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(
  rest.get('/greeting', (req, res, ctx) => {
    return res(ctx.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(
    rest.get('/greeting', (req, res, ctx) => {
      return res(ctx.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()
})

Let's do a breakdown of each line of code step by step:

1. Imports: we need to import the API mocking utilities from Mocker Server Worker (MSW), including the rest and setupServer utilities:

import {rest} from 'msw'
import {setupServer} from 'msw/node'

We also import specific react-testing methods from the @testing-library/react package:

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

Along with that, we need custom Jest matchers from jest-dom:

import '@testing-library/jest-dom'

2. Mocking up: we use the setupServer function from msw to mock an API request that our tested component makes. This happens in the following steps:

  1. Declaring which API requests to mock:
    const server = setupServer(
      rest.get('/greeting', (req, res, ctx) => {
        return res(ctx.json({greeting: 'hello there'}))
     }),
    )
  2. Establishing API mocking before all tests:
    beforeAll(() => server.listen())
    afterEach(() => server.resetHandlers())
    afterAll(() => server.close())
  3. Using the test case to override the initial "Get /greeting" request handler to return a 500 Server Error:

    test('handles server error', async () => {
     server.use(
        rest.get('/greeting', (req, res, ctx) => {
             return res(ctx.status(500))
            }),
        )
    ...
    })
  4. 3. Arranging: by using the render method, we make sure it renders a React element into the DOM:

    render(<Fetch url="/greeting" />)

    4. Simulating user actions: with the fireEvent method, it allows you to fire events in order to simulate user actions:

    fireEvent.click(screen.getByText('Load Greeting'))
    
    await waitFor(() =>
      screen.getByRole('heading'),
    )

    Here, we wait until the get request promise resolves and the component calls setState and re-renders. waitFor function is used to wait until the callback doesn't throw an error, and getByRole throws an error if it cannot find an element in DOM.

    5. Asserting: here, we assert that the alert message is correct using the toHaveTextContent matcher from jest-dom:

    expect(screen.getByRole('alert')).toHaveTextContent('Oops, failed to fetch!')

    Finally, we also assert that the button isn't disabled using toBeDisabled, another custom matcher from jest-dom:

    expect(screen.getByRole('button')).not.toBeDisabled()

    3. render Options

    The render method is used to render into a container appended to doument.body:

    import {render} from '@testing-library/react'
    import '@testing-library/jest-dom'
    
    test('renders a message', () => {
      const {asFragment, getByText} = render(<Greeting />)
      expect(getByText('Hello, world!')).toBeInTheDocument()
      expect(asFragment()).toMatchInlineSnapshot(`
        <h1>Hello, World!</h1>
      `)
    })

    But there are some extra options available to this method, such as:

    1. container: if you provide your own HTMLElement container via this option, it will not automatically be appended to the document.body. For example, if you’re testing a tablebody element, it can't be a child of a div, so in this case, you can specify a table as the render container:

    const table = document.createElement('table')
    
    const {container} = render(<TableBody {...props} />, {
      container: document.body.appendChild(table),
    })

    2. baseElement: this is used as the element for the queries as well as what is printed when you use debug(). If container is specified, it defaults to the same; else, it defaults to document.body.

    3. hydrate: if this is set to true, then it renders with ReactDOM.hydrate, which is used to hydrate a container whose HTML contents were rendered by ReactDOMServer. It's used when the app uses server-side rendering and to mount the components.

    4. legacyRoot: if you're dealing with a legacy app that requires rendering like in React v. 17 (with ReactDOM.render), then you enable this option by setting legacyRoot: true.

    5. queries: these are the queries to bind, and they override the default setting from DOM Testing Library unless they're merged.

    Here's an example where we have 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},
    })

    4. renderHook Options

    renderHook is a wrapper around render with a custom test component. Here's an example:

    import {renderHook} from '@testing-library/react'
    
    test('returns logged in user', () => {
      const {result} = renderHook(() => useLoggedInUser())
      expect(result.current).toEqual({name: 'Alice'})
    })

    But it comes with two essential options, one of which is the same wrapper option that is also available for render, where it's used to pass a React component to have it rendered around the inner element.

    And the other option is the initialProps option which declares the props that are passed to the render-callback when it's first invoked. One thing to note here is that these props will not be passed if you call rerender without props:

    import {renderHook} from '@testing-library/react'
    
    test('returns logged in user', () => {
      const {result, rerender} = renderHook(({name} = {}) => name, {
        initialProps: {name: 'Alice'},
      })
      expect(result.current).toEqual({name: 'Alice'})
      rerender()
      expect(result.current).toEqual({name: undefined})
    })

    5. renderHook Result

    The renderHook method returns an object that has two properties:

    1. result: this holds the value of the most recently committed returned value of the render callback as shown below:

    import {renderHook} from '@testing-library/react'
    
    const {result} = renderHook(() => {
      const [name, setName] = useState('')
      React.useEffect(() => {
        setName('Alice')
      }, [])
    
      return name
    })
    
    expect(result.current).toBe('Alice')

    The value is held in result.current. We can think of the result property as a ref for the most recently committed value.

    2. rerender: this one renders the previously rendered callback with the new props:

    import {renderHook} from '@testing-library/react'
    
    const {rerender} = renderHook(({name = 'Alice'} = {}) => name)
    rerender({name: 'Bob'})

    Advance Level - React Testing Library Cheat Sheet


    1. Adding custom queries

    Before we jump to know about how to make a custom query, we should know that in most cases, we won't need to have one. But if we really need one, we need to make sure our custom queries encourage us to test in a user-centric way without testing the implementation details.

    With that being clear, we can define our own custom queries in the following ways:

    1. The helper functions are used to implement the default queries exposed by the DOM Testing Library. Like with the previously discussed render method of React Testing Library. Here, we can add queries to the options configuration object.

    In the example below, we can see how to override the default testId queries to use a different data attribute:

    const domTestingLib = require('@testing-library/dom')
    const {queryHelpers} = domTestingLib
    
    export const queryByTestId = queryHelpers.queryByAttribute.bind(
      null,
      'data-test-id',
    )
    export const queryAllByTestId = queryHelpers.queryAllByAttribute.bind(
      null,
      'data-test-id',
    )
    
    export function getAllByTestId(container, id, ...rest) {
      const els = queryAllByTestId(container, id, ...rest)
      if (!els.length) {
        throw queryHelpers.getElementError(
          `Unable to find an element by: [data-test-id="${id}"]`,
          container,
        )
      }
      return els
    }
    
    export function getByTestId(container, id, ...rest) {
      // result >= 1
      const result = getAllByTestId(container, id, ...rest)
      if (result.length > 1) {
        throw queryHelpers.getElementError(
          `Found multiple elements with the [data-test-id="${id}"]`,
          container,
        )
      }
      return result[0]
    }
    
    // re-export with overrides
    module.exports = {
      ...domTestingLib,
      getByTestId,
      getAllByTestId,
      queryByTestId,
      queryAllByTestId,
    }

    2. Another method to have a custom query is by using the buildQueries helper. This allows you to create custom queries with all the standard queries in the testing library. Here's an example:

    import {queryHelpers, buildQueries} from '@testing-library/react'
    
    const queryAllByDataCy = (...args) =>
      queryHelpers.queryAllByAttribute('data-cy', ...args)
    
    const getMultipleError = (c, dataCyValue) =>
      `Found multiple elements with the data-cy attribute of: ${dataCyValue}`
    const getMissingError = (c, dataCyValue) =>
      `Unable to find an element with the data-cy attribute of: ${dataCyValue}`
    
    const [
      queryByDataCy,
      getAllByDataCy,
      getByDataCy,
      findAllByDataCy,
      findByDataCy,
    ] = buildQueries(queryAllByDataCy, getMultipleError, getMissingError)
    
    export {
      queryByDataCy,
      queryAllByDataCy,
      getByDataCy,
      getAllByDataCy,
      findAllByDataCy,
      findByDataCy,
    }

    In the example above, a new set of query variants was created for getting elements by data-cy, a test ID convention.

    After making the custom queries, we can then use them in any render call using the queries option. To add them globally, we can do the same by defining a custom render method as shown below:

    import {render, queries} from '@testing-library/react'
    import * as customQueries from './custom-queries'
    
    const customRender = (ui, options) =>
      render(ui, {queries: {...queries, ...customQueries}, ...options})
    
    // re-export everything
    export * from '@testing-library/react'
    
    // override render method
    export {customRender as render}

    Now it's ready to use like any other query as follows:

    const {getByDataCy} = render(<Component />)
    
    expect(getByDataCy('my-component')).toHaveTextContent('Hello')

    2. Skipping auto cleanup

    React Testing Library's cleanup utility unmounts React trees that were mounted before with render. For example, if we are using the AVA testing framework, then we use test.afterEach Hook as:

    import {cleanup, render} from '@testing-library/react'
    import test from 'ava'
    
    test.afterEach(cleanup)
    
    test('renders into document', () => {
      render(<div />)
      // ...
    })

    Now, this cleanup is called after each test automatically if the testing framework supports the afterFetch global. But, we can choose to skip the auto cleanup by setting the RTL_SKIP_AUTO_CLEANUP environment variable to `true` as shown below with <a href="https://github.com/kentcdodds/cross-env" target="_blank">cross-env:</a>

    cross-env RTL_SKIP_AUTO_CLEANUP=true jest

    A simpler way is to import @testing-library/react/dont-cleanup-after-each before @testing-library/react in your test code. Here's an example with Jest's setupFiles config:

    {
      setupFiles: ['@testing-library/react/dont-cleanup-after-each']
    }

    3. Migrating from Enzyme to React Testing Library

    The recommended way to ensure a successful migration is to do it incrementally by running two test libraries side by side in the same app and then porting our Enzyme tests to React Testing Library one by one.

    Let's take this step by step:

    1. Installing React Testing Library

    First, we install the testing library along with the jest-dom helper:

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

    2. Importing React Testing Library to your test

    If you are using Jest, then you need to import the following modules to your test file:

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

    As for the test structure, it's the same as you would write with Enzyme:

    test('test title', () => {
      // Your tests come here...
    })

    3. Migrating the tests

    Let's say we have a Welcome component that shows a welcome message:

    const Welcome = props => {
      const [values, setValues] = useState({
        firstName: props.firstName,
        lastName: props.lastName,
      })
    
      const handleChange = event => {
        setValues({...values, [event.target.name]: event.target.value})
      }
    
      return (
        <div>
          <h1>
            Welcome, {values.firstName} {values.lastName}
          </h1>
    
          <form name="userName">
            <label>
              First Name
              <input
                value={values.firstName}
                name="firstName"
                onChange={handleChange}
              />
            </label>
    
            <label>
              Last Name
              <input
                value={values.lastName}
                name="lastName"
                onChange={handleChange}
              />
            </label>
          </form>
        </div>
      )
    }
    
    export default Welcome

    This component gets a name from props showing a welcome message in an h1 element. It also has a text input which users can change to a different name. Now, if we want to test "to render the component and check that the h1 value is correct," then we have the following Enzyme test:

    test('has correct welcome text', () => {
      const wrapper = shallow(<Welcome firstName="John" lastName="Doe" />)
      expect(wrapper.find('h1').text()).toEqual('Welcome, John Doe')
    }) 

    And in React Testing Library as:

    test('has correct welcome text', () => {
      render(<Welcome firstName="John" lastName="Doe" />)
      expect(screen.getByRole('heading')).toHaveTextContent('Welcome, John Doe')
    })

    Enzyme's shallow renderer doesn't render sub-components, while React Testing Library's render method is more similar to Enzyme's mount method. We don't need to assign the render result to a variable or a wrapper in React Testing Library. We can use the screen object to access the rendered output.


    4. Querying within elements

    To query elements, we can use the within utility function (which is an alias to getQueriesForElement). This takes a DOM element and binds it to the raw query functions, and this allows them to be used without specifying a container.

    For example, to get the text "Hello" only within a section called "messages", we can do something like this:

    import {render, within} from '@testing-library/react'
    
    const {getByText} = render(<MyComponent />)
    const messages = getByText('messages')
    const helloMessage = within(messages).getByText('hello')

    5. Integration testing with React Testing Library

    It's common to write unit tests for React components. But since writing React apps is about composing components, unit tests don't ensure a bug-free app.

    That's why we need some integration tests that can help preserve peace of mind while we make changes to our React app because they ensure that the composition of components results in a better UX.

    Now, to write integration tests with React Testing Library, we will take an example where we implement a scenario of the user entering their GitHub username and then the app displaying the list of their associated public repos.

    Let's see how to do this step by step after installing the required testing packages:

    1. Creating an integration test suite file: here, we create a file named viewGitHubRepositoriesByUsername.spec.js inside the ./test folder of our app so that Jest automatically picks it up.

    2. Importing dependencies in the test file: we need to import the following dependencies to work:

    import {
     render,
     cleanup,
     waitForElement
    } from '@testing-library/react'; 
    import userEvent from '@testing-library/user-event';
    
    import 'jest-dom/extend-expect'; 
    
    // Used to mock GitHub API
    import nock from 'nock';
    
    // Our test data
    import {
     FAKE_USERNAME_WITH_REPOS,
     FAKE_USERNAME_WITHOUT_REPOS,
     FAKE_BAD_USERNAME,
     REPOS_LIST
    } from './fixtures/github'; 
    
    import App from '../App'; 

    3. Setting up the test suite: we first need to mock the GitHub API before we start writing the tests. Then, after each test, we must clean the test React DOM so that the tests start from a clean point.

    Here, we use the describe and beforeAll utilities to do the same, and to use the nock function, we are able to call the API:

    describe('view GitHub repositories by username', () => {
     beforeAll(() => {
       nock('https://api.github.com')
         .persist()
         .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`)
         .query(true)
         .reply(200, REPOS_LIST);
     });
    
     afterEach(cleanup);

    The describe block specifies the integration test use case and the flow variants. So in those flow variants, we need to test the following:

    • User enters a valid username that has public repos.
    • User enters a valid username with no associated public repos.
    • User enters a username that doesn't exist on GitHub.
    describe('when GitHub user has public repositories', () => {
       it('user can view the list of public repositories for entered GitHub username', async () => {
         //...
       });
     });
    
    
     describe('when GitHub user has no public repositories', () => {
       it('user is presented with a message that there are no public repositories for entered GitHub username', async () => {
         //...
       });
     });
    
     describe('when GitHub user does not exist', () => {
       it('user is presented with an error message', async () => {
        //...
       });
     });
    });

    The it blocks use async callback as needed.

    4. Writing the flow test

    First, we need to check whether the app is rendered or not; for that, we can use the render method from the @testing-library/react package:

     const { getByText, getByPlaceholderText, queryByText } = render(<app>);</app>

    Now, for the first step of the flow test, the user is presented with a username field and types a username string:

    userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);

    The userEvent helper utility has a type method that imitates the user behavior when they type text in a text field. The getByPlaceholder query method returned earlier from render allows us to find the DOM element by placeholder text.

    Up next, we have to test whether the user clicks on the submit button and expects a list of repos:

    userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
    getByText('repositories.header');

    The userEvent.click method imitates the user clicking a DOM element, and the getByText query finds a DOM element by its text. The closest modifier ensures that we have indeed selected the right element.

    Next, we want to make sure that the app finishes fetching the repo list and displays it to the user:

    await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => {
       elementsToWaitFor.push(getByText(repository.name));
       elementsToWaitFor.push(getByText(repository.description));
       return elementsToWaitFor;
     }, []));

    The waitForElement async method is used to wait for a DOM update that will render the provided assertion as the method parameter is true. Here, we assert that the app displays the name and description for every repo that the mocked GitHub API returns.

    And finally, we want to test that our app no longer displays a fetching indicator and instead should display an error message:

    expect(queryByText('repositories.loadingText')).toBeNull();
    expect(queryByText('repositories.error')).toBeNull();

    The toBeNull modifier checks for the non-existence of an element.


    Conclusion

    In this React Testing Library cheat sheet, we covered why this library is preferred over others, what problems it solves, coding a basic component render test, and in the end, accomplishing a complete integration test step by step.

    We hope this React Testing Library cheat sheet will help you use this awesome library in your next React testing sprint.

// Find jobs by category

You've got the vision, we help you create the best squad. Pick from our highly skilled lineup of the best independent engineers in the world.

Copyright @2023 Flexiple Inc