Lessons I learned as a Jest and React Testing Library beginner
Tags: jest, reacttestinglibrary, beginners, testing
Published: 2022-08-23 15:01:00

I recently began using `Jest` and `React Testing Library` (rtl). This article is what I would have wanted to read after I first started. It is not a tutorial but a series of solutions to specific problems you will run into. I structured everything in 4 blocks: 

1. `queries` 
2. `matchers`
3. `setup functions`
4. `mocks` 

This article covers the first 3 blocks. I will write about `mocking` in a [later series](https://dev.to/peterlidee/series/19703). You can find all the code in this article on [github](https://github.com/peterlidee/testing-app).
___

## 1. Queries

`React Testing Library` provides [queries](https://testing-library.com/docs/queries/about/#overview) and [guidelines](https://testing-library.com/docs/queries/about/#priority) on how to use them. Here are some tips and tricks regarding queries.

### 1.1 you can still use `querySelector`

Before you start adding `data-testid` to everything, remember that `Jest` is just javascript. The `expect()` function expects an DOM element to be passed in. So you can use `querySelector`. Call `querySelector` on `container`, returned by the `render` function.

```jsx
// the component
function Component1(){
  return(
    <div className="Component1">
      <h4>Component 1</h4>
      <p>Lorum ipsum.</p>
    </div>
  )
}
```
```javascript
// the test
test('Component1 renders', () => {
  // destructure container out of render result
  const { container } = render(<MyComponent />)
  // true
  // eslint-disable-next-line
  expect(container.querySelector('.Component1')).toBeInTheDocument()
})
```
`querySelector` is good for testing your *generic* html. But, always use specific rtl queries if possible: f.e. inputs, buttons, images, headings,...

Beware, using `querySelector` will make eslint yell at you, hence the `// eslint-disable-next-line`.

### 1.2 How to query multiple similar elements

Let's take a component with 2 buttons, add and subtract. How do you query these buttons? You have 2 options:

```jsx
// the component
function Component2(){
  return(
    <div className="Component2">
      <h4>Component 2</h4>
      <button>add</button>
      <button>subtract</button>
    </div>
  )
}
```

#### 1.2.1 Use the options parameter on query

Most of the rtl queries have an optional [`options`](https://testing-library.com/docs/queries/byrole#options) parameter. This lets you select the specific element you want. In this case we use `name`.

```javascript
test('Component2 renders', () => {
  render(<Component2 />)
  // method 1
  expect(screen.getByRole('button', { name: 'subtract' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'add' })).toBeInTheDocument()
})
```

#### 1.2.2 Use the getAll query

`React Testing Library` has built-in queries for multiple elements, `getAllBy...`. These queries return an array.

```javascript
test('Component2 renders', () => {
  render(<Component2 />)
  // method 2
  const buttons = screen.getAllByRole('button')
  expect(buttons[0]).toBeInTheDocument()
  expect(buttons[1]).toBeInTheDocument()
})
```

### 1.3 Find the correct role

Some html elements have specific ARIA roles. You can find them on this [w3.org page](https://www.w3.org/TR/html-aria/#docconformance). (Bookmark tip) Some examples:

```jsx
// the component
function Component3(){
  const [ count, setCount ] = useState(0)
  return(
    <div className="Component3">
      <h4>Component 3</h4>
      <input type="number" value={count} onChange={(e) => setCount(parseInt(e.target.value))} />
    </div>
  )
}
```
```javascript
// the test
test('Component3 renders', () => {
  render(<Component3 />)
  // get the heading h3
  expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument()
  // get the number input
  expect(screen.getByRole('spinbutton')).toBeInTheDocument()
})
```
___
## 2. matchers

The core of a test is the matcher, the `expect()` statement followed by a `.toBe...` or `.toHave...`. This is `Jest`, it's not `React Testing Library`. Invest some time in [getting to know these matchers](https://jestjs.io/docs/expect) (another bookmark tip).

On top of these `Jest` matchers, there is an additional library: [`jest-dom`](https://github.com/testing-library/jest-dom#table-of-contents) (yes, more bookmarks).

> `jest-dom` is a companion library for Testing Library that provides custom DOM element matchers for `Jest`.

So, `jest-dom` provides more matchers and they are quite handy. Let's look at some of them in action. I wrote some tests in `jest` followed by the `jest-dom` equivalent.

```jsx
// the component
function Component4(){
  const [ value, setValue ] = useState("Wall-E")
  return(
    <div className="Component4">
      <h4>Component 4</h4>
      <label htmlFor="movie">Favorite Movie</label>
      <input 
        id="movie"
        value={value} 
        onChange={(e) => setValue(e.target.value)} 
        className="Component4__movie"
        style={{ border: '1px solid blue', borderRadius: '3px' }} 
        data-value="abc" />
    </div>
  )
}
```
```javascript
test('Component4 renders', () => {
  
  render(<Component4 />)
  const input = screen.getByLabelText('Favorite Movie')
  const title = screen.getByRole('heading', { level: 4 })

  // we already used .toBeInTheDocument(), this is jest-dom matcher
  expect(input).toBeInTheDocument()

  // test for class with jest
  expect(input.classList.contains('Component4__movie')).toBe(true)
  // test for class with jest-dom
  expect(input).toHaveClass('Component4__movie')
  
  // test for style with jest
  expect(input.style.border).toBe('1px solid blue')
  expect(input.style.borderRadius).toBe('3px')
  // test for style with jest-dom
  expect(input).toHaveStyle({
    border: '1px solid blue', 
    borderRadius: '3px',
  })

  // test h4 value with jest
  expect(title.textContent).toBe("Component 4")
  // test h4 value with jest-dom
  expect(title).toHaveTextContent("Component 4")

  // test input data attribute with jest
  expect(input.dataset.value).toEqual('abc')
  // test input data attribute with jest-dom
  expect(input).toHaveAttribute('data-value', 'abc')
})
```
___
## 3. render setups

Writing tests for components can be repetitive and time consuming. Let's take a look at how a setup function can make your code more DRY (don't repeat yourself).

We will be testing a component that displays a value. It has an add and a subtract button and takes an increment (number) as prop. The buttons add or subtract the increment from the value.

```jsx
// the component
function Component5({ increment }){
  const [ value, setValue ] = useState(0)
  return(
    <div className="Component5">
      <h4>Component 5</h4>
      <div className="Component5__value">{value}</div>
      <div className="Component5__controles">
        <button onClick={e => setValue(prevValue => prevValue - increment)}>subtract</button>
        <button onClick={e => setValue(prevValue => prevValue + increment)}>add</button>
      </div>
    </div>
  )
}
```

We will run 3 tests on this component: test if the component renders, test if buttons work, test increment. We will first run *not DRY* code. After that, we will refactor the tests with a setup function.

```javascript
// the tests
describe('Component5 (not DRY)', () => {
  test('It renders correctly', () => {
    const { container } = render(<Component5 increment={1} />)

    // get the elements
    // eslint-disable-next-line
    const valueEl = container.querySelector('.Component5__value')
    const subtractButton = screen.getByRole('button', { name: 'subtract' })
    const addButton = screen.getByRole('button', { name: 'add' })
    
    // do the tests
    // eslint-disable-next-line
    expect(container.querySelector('.Component5')).toBeInTheDocument()
    expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Component 5')
    expect(valueEl).toBeInTheDocument()
    expect(valueEl).toHaveTextContent('0')
    expect(subtractButton).toBeInTheDocument()
    expect(addButton).toBeInTheDocument()
  })

  test('It changes the value when the buttons are clicked', () => {
    const { container } = render(<Component5 increment={1} />)

    // get the elements
    // eslint-disable-next-line
    const valueEl = container.querySelector('.Component5__value')
    const subtractButton = screen.getByRole('button', { name: 'subtract' })
    const addButton = screen.getByRole('button', { name: 'add' })

    // test default value
    expect(valueEl).toHaveTextContent('0')
    // test addbutton
    userEvent.click(addButton)
    expect(valueEl).toHaveTextContent('1')
    // test subtract button
    userEvent.click(subtractButton)
    expect(valueEl).toHaveTextContent('0')
  })

  test('It adds or subtract the increment 10', () => {
    const { container } = render(<Component5 increment={10} />)

    // get the elements
    // eslint-disable-next-line
    const valueEl = container.querySelector('.Component5__value')
    const subtractButton = screen.getByRole('button', { name: 'subtract' })
    const addButton = screen.getByRole('button', { name: 'add' })

    // test addbutton
    userEvent.click(addButton)
    expect(valueEl).toHaveTextContent('10')
    // test subtract button
    userEvent.click(subtractButton)
    expect(valueEl).toHaveTextContent('0')
  })
})
```

As you can see, there is a lot of duplication. We make the same render and the same queries in all tests. We will now rewrite these tests. We start by adding this function in root of the file:

```javascript
function setup(props){
  const { container } = render(<Component5 {...props} />)
  return{
    // eslint-disable-next-line
    valueEl: container.querySelector('.Component5__value'),
    subtractButton: screen.getByRole('button', { name: 'subtract' }),
    addButton: screen.getByRole('button', { name: 'add' }),
    container,
  }
}
```

Let me walk you through this function:

1. We moved the `render()` inside our `setup` function. When `setup` is called, the component renders.

2. The `render()` still returns `container` so we have access to that element inside our `setup` function.

3. We now spread the `setup` argument (props, an object) into our component: `{...props}`. This pattern allows to use the same `setup` function with different props.

    ```javascript
    setup({ increment: 1 })
    // calls render(<Component5 increment="1">)
    setup({ increment: 5 })
    // calls render(<Component5 increment="5">)
    ```
4. From our `setup` function, we return an object with all our frequently used queries (the buttons and the value element). This gives us access to these queries inside the test, where the `setup` function is called.

    ```javascript
    test('It renders', () => {
      const { valueEl, subtractButton, addButton } = setup({ increment: 1 })
      // do tests with these elements
    })
    ```
5. Lastly, I also placed `container` on the return object. This gives us access to container inside `test()` for queries that for example you only use once.

    ```javascript
    test('It renders', () => {
      const { container } = setup({ increment: 1 })
      // eslint-disable-next-line
      expect(container.querySelector('.Component5')).toBeInTheDocument()
    })
    ```

To conclude: the updated test with this setup function:

```javascript
// the setup function
function setup(props){
  const { container } = render(<Component5 {...props} />)
  return{
    // eslint-disable-next-line
    valueEl: container.querySelector('.Component5__value'),
    subtractButton: screen.getByRole('button', { name: 'subtract' }),
    addButton: screen.getByRole('button', { name: 'add' }),
    container,
  }
}
// the tests
describe('Component 5 (DRY)', () => {
  test('It renders', () => {
    const { container, valueEl, subtractButton, addButton } = setup({ increment: 1 })
    // do the tests
    // eslint-disable-next-line
    expect(container.querySelector('.Component5')).toBeInTheDocument()
    expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Component 5')
    expect(valueEl).toBeInTheDocument()
    expect(valueEl).toHaveTextContent('0')
    expect(subtractButton).toBeInTheDocument()
    expect(addButton).toBeInTheDocument()
  })

  test('It changes the value when the buttons are clicked', () => {
    const { valueEl, subtractButton, addButton } = setup({ increment: 1 })

    // test default value
    expect(valueEl).toHaveTextContent('0')
    // test addbutton
    userEvent.click(addButton)
    expect(valueEl).toHaveTextContent('1')
    // test subtract button
    userEvent.click(subtractButton)
    expect(valueEl).toHaveTextContent('0')
  })

  test('It adds or subtract the increment 10', () => {
    const { valueEl, subtractButton, addButton } = setup({ increment: 10 })

    // test addbutton
    userEvent.click(addButton)
    expect(valueEl).toHaveTextContent('10')
    // test subtract button
    userEvent.click(subtractButton)
    expect(valueEl).toHaveTextContent('0')
  })
})
```

This may still seem like a lot of code but it is a lot cleaner. This pattern will save you a lot of time and avoids repetition.
___
## Conclusion

We looked into testing `queries`, `matchers` and `setup functions`. I offered solutions for problems you may run into. I hope this gives you a better practical knowledge of testing react components.

I wrote a series on [mocking `React`](https://dev.to/peterlidee/series/19703) that is a good follow up on this article.