React component testing with Jest and React Testing Library
Tags: react, testing
Published: 2021-09-07 11:07:13

![Interface experiments](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/st1vviwju1cx022bnd1s.png)

Testing React components gives you confidence a component will work when the user interacts with it. As a junior full-stack developer on my first job, I found it extremely useful in helping me understand our current codebase as well as allowing me to add value while learning.

This article is a summary of the information I found useful during my research and the answer to some challenges I came across. I don't hope to re-invent the wheel but to help others in a similar stage of their career. It's also assumed that you have _some_ experience in writing tests.

### Why [Jest](https://jestjs.io/) and [RTL (React Testing Library)](https://testing-library.com/docs/react-testing-library/intro/)?

React openly recommends Jest as a test runner (perhaps because they maintain it) and RTL as their testing utility of choice. Jest testing is very fast, it's easy to set up and it has many powerful features such as mock functions which allow you to replace a specific function and return a desirable value or to check how the test subject is executing the function. RTL is very simple to set up, easy to make queries (including asynchronously) and because of how it was built, it'll help you write good tests.

[Jest-Dom](https://testing-library.com/docs/ecosystem-jest-dom/) is not required but makes writing tests much easier because it extends Jest matchers (methods that let you test values in different ways e.g. `toBe()`, `toHaveBeenCalled()`) and allows you to write clearer tests.

Another popular tool is [Enzyme](https://enzymejs.github.io/enzyme/), but many believe that it can lead to bad testing practices. The main concern is that Enzyme offers extra utilities that allow you to test the internal workings of a component (e.g. read and set state of the component). The team at React tests React; therefore, there is no need for you to test React’s functionality such as state, `componentDidMount`, etc. The same goes for other libraries you may use.

### What to test?

When component testing in React, the focus should be on replicating how the user would interact with the React component. This means that we should test for what the user should or should not see, and how they are meant to interact with the app once it renders (e.g. that the value of a search/input field can be changed) instead of testing implementation (e.g. was `componentDidMount` called x number of times).

Some good questions to ask yourself when writing tests are:

- What does the component render? Also, does it render differently under different conditions?
  - This is what the user will see and potentially interact with. By thinking about it, you will also realise that users should access and see different information depending on certain conditions being met
- What happens when the user interacts with the component?
  - These are the parts of the app which the user will click, write into, etc. and they’ll expect something to happen. Tests should be written to prove that whatever is meant to happen does so when the event is triggered!
- When a function is passed in as a prop, how does the component use it?
  - You may need to recreate the behaviour of this function by using the Jest mock concept to know if the function has been called and the correct values were used

### How to write a test?

So, onto the interesting part, how to test React components with Jest...

RTL’s most used functions are:

- `render` – which renders the component
- `cleanup` – which unmounts the React DOM tree that was mounted with `render`, and
- `fireEvent` – to fire events like a click.

Jest's most used functions are:

- `expect` along with a matcher
- `jest.fn()` to mock a function directly
- `jest.spyOn()` to mock an object method, and
- `jest.mock()` for an entire module.

The test should be structured as follows:

1. Declare all `jest.fn()`/`spyOn()`/`mock()` with or without mocked implementations
2. Call RTL’s `render` function with the test subject as an argument – provide context whenever the component consumes a context. Also, if React-Router Link is used in this component, an object with a property wrapper and value MemoryRouter (imported from React-Router) must be passed as the second argument. Optionally wrap the component in MemoryRouter tags
3. Query the React DOM tree by using RTL’s query functions (e.g. `getByRole()` ) and check the values by call
4. Check values queried by calling `expect()` along with the relevant matcher. To replicate user interaction use `fireEvent`

RTL also returns a `debug()` method when render is called. Debug is fantastic for checking what is rendered in the React tree for situations like debugging your tests.

We will use the code below (a search field) as our example of a React component:

```tsx
render = () => {
  const {
    validateSelection,
    minCharacters,
    placeholder,
    inputFluid,
    inputLabel,
    clear
  }: any = this.props

  const { isLoading, value, results } = this.state

  const icon = validateSelection ? (
    <Icon name="check" color="green" />
  ) : (
    <Icon name="search" />
  )

  return (
    <Search
      minCharacters={minCharacters}
      loading={isLoading}
      icon={icon}
      onResultSelect={this.onResultSelect}
      onSearchChange={this.onSearchChange}
      results={results}
      value={clear ? null : value}
      fluid
      placeholder={placeholder}
      input={{ fluid: inputFluid, label: inputLabel }}
    />
  )
}
```

Above we are destructuring props and state. We are also returning a [Semantic UI React](https://react.semantic-ui.com/) `Search` module. In essence, the above will render an input field. When changed, it will call `onSearchChange` and Semantic UI React will automatically pass two arguments, `event` and `data` (all props, including current value). One of `onSearchChange`’s jobs is to call an API and return results that match the current value.

Below are the tests we built for this component.

```jsx
import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import { render, cleanup, fireEvent } from '@testing-library/react'
import SearchField from './SearchField'

afterEach(cleanup)
jest.useFakeTimers()

test('<SearchField />', () => {
  const handleResultSelectMock = jest.fn()
  const apiServiceMock = jest
    .fn()
    .mockImplementation(() =>
      Promise.resolve({ entity: { success: true, data: ['hello', 'adios'] } })
    )

  const { getByRole, debug } = render(
    <SearchField
      handleResultSelect={handleResultSelectMock}
      apiService={apiServiceMock}
    />
  )

  const input = getByRole('textbox')
  expect(apiServiceMock).not.toHaveBeenCalled()
  expect(input).toHaveValue('')

  fireEvent.change(input, { target: { value: 'search' } })
  expect(input).toHaveValue('search')
  jest.advanceTimersByTime(600)

  expect(apiServiceMock).toHaveBeenCalledWith('search')
  expect(apiServiceMock).toHaveBeenCalledTimes(1)
  debug()
})
```

#### What is happening in the example above?

We imported all dependencies needed to test this component.

- Jest DOM - to extend jest matchers
- `render`, `cleanup`, `fireEvent` \- React Testing Library utilities
- `SearchField` \- the React component being tested

```js
import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import { render, cleanup, fireEvent } from '@testing-library/react'
import SearchField from './SearchField'
```

We called Jest's function `afterEach` and passed RTL's method `cleanup` as an argument. `cleanup` will make sure that there are no memory leaks between tests by unmounting everything mounted by RTL's `render` method. We also called Jest's `useFakeTimers` function to mock timer functions.

```js
afterEach(cleanup)
jest.useFakeTimers()
```

The component requires two props which should be functions. Therefore, we started by mocking two functions that will be passed to the component as props - `handleResultSelectMock` and `apiServiceMock`. `handleResultSelectMock` will be passed to `handleResultSelect` and `apiServiceMock` to `apiService`. Then, RTL's `render` method is called with the SearchField component as the argument.

```jsx
test('<SearchField />', () => {
  const handleResultSelectMock = jest.fn()
  const apiServiceMock = jest
    .fn()
    .mockImplementation(() =>
      Promise.resolve({ entity: { success: true, data: ['hello', 'adios'] } })
    )

  const { getByRole, debug } = render(
    <SearchField
      handleResultSelect={handleResultSelectMock}
      apiService={apiServiceMock}
    />
  )
})
```

#### There will be times when the component being tested will require a `wrapper: Memory Router` or a `context` to render successfully. Take a look at the example below:

```jsx
const { getByTestId, container } = render(
  <UserContext.Provider value={context}>
    <MainLoggedIn
      config={{
        get: jest.fn().mockImplementation(() => ({
          globalMenu: [{ requiredPermissions: ['Navbar'] }]
        }))
      }}
      history={{ history: ['first_history', 'second_history'] }}
      children={['first_child', 'second_child']}
    />
  </UserContext.Provider>,
  { wrapper: MemoryRouter }
)
```

After `render` is called, we should query the React DOM tree and find the elements we want to test. Below we used `getByRole`, but RTL offers many other query selectors functions.

```js
const input = getByRole('textbox')
```

To check values, start with the function `expect` along one of the several matchers. Here we started by checking that the apiServiceMock has **not** been called, then checks that the input field is an empty string (`value = ''`) when the component first renders.

```js
expect(apiServiceMock).not.toHaveBeenCalled()
expect(input).toHaveValue('')
```

An event is fired using the function `change` of RTL's `fireEvent` to replicate the user's behaviour. This event will update the value of the input field from `''` to `'search'`. You can replicate other scenarios by using other `fireEvent` methods such as `click()`, `mouseOver()`. Jest's `advanceTimersByTime` method is called to move the mock timer forward by 600ms hence the number 600 is passed as an argument. `advanceTimersByTime` makes sure that tasks that have been queued by a timer function and would be executed within the given time (600ms in this case) will be executed.

```js
fireEvent.change(input, { target: { value: 'search' } })
expect(input).toHaveValue('search')
jest.advanceTimersByTime(600)
```

After firing the event, we expect a few things to happen, the `apiServiceMock` function to be called once, and the argument passed to `apiServiceMock` to match the current input's value.

```js
expect(apiServiceMock).toHaveBeenCalledWith('search')
expect(apiServiceMock).toHaveBeenCalledTimes(1)
debug()
```

Lastly, the `debug` function is called to check what is rendered in the React tree and help debug the tests.

### Summary

- Small and straightforward tests are better.
- Test each component independently.
- Focus on testing what the user will see and how they will interact with the component.
- Start building the tests _after_ assessing what needs to be tested.

### More on the topic:

- [Jest cheat sheet](https://github.com/sapegin/jest-cheat-sheet)
- [Modern React testing, part 3: Jest and React Testing Library](https://blog.sapegin.me/all/react-testing-3-jest-and-react-testing-library/)
- [The Right Way to Test React Components](https://www.freecodecamp.org/news/the-right-way-to-test-react-components-548a4736ab22/)
- [React Testing Library - Introduction](https://testing-library.com/docs/react-testing-library/intro/)
- [Configuring Jest](https://jestjs.io/docs/configuration)
- [Mock Functions or Spies Demystified - How Does `jest.fn()` Work?](<https://www.pluralsight.com/guides/how-does-jest.fn\(\)-work>)