Beginner's Guide to Jest Testing in React
Tags: javascript, testing, react, codenewbie
Published: 2021-05-13 17:23:25

In my last post, [A Beginner's Guide to Unit-testing with Jest](https://dev.to/dsasse07/a-beginner-s-guide-to-unit-testing-with-jest-45cc), I walked through getting started with testing in javascript using the Jest testing library. Here, I hope to expand on what was already discussed about matchers and expectations, and the purpose of test implementation with an example of how to write basic tests for React components.

Writing tests for React components with Jest follows the same similar structure of a `describe` function containing `test` blocks with `expect` functions and matchers. However, instead of testing the functionality of individual JS functions, we need to ensure that React components are rendering properly and that user interactions with the component occur as expected. For a detailed guide on the basic setup for Jest testing and it purposes, please see my previous post, [A Beginner's Guide to Unit-testing with Jest](https://dev.to/dsasse07/a-beginner-s-guide-to-unit-testing-with-jest-45cc).

### Getting Started

We will walk through the process of setting up a basic React App with interactive elements such as a counter with increment/decrement buttons, and a form to post text to the DOM. I will walk through writing the Jest tests and the rest of the code here, but you can view [the repo containing all of the code as well](https://github.com/dsasse07/jest-react-example).


## Contents
* [Setting Up The App](#setting-up-the-app)
* [Anatomy of the Default React Test](#anatomy-of-the-default-react-test)
* [Planning the Tests](#planning-the-tests)
* [Describe the Tests](#describe-the-tests)
 * [Counter Tests](#counter-tests)
 * [Form Tests](#form-tests)
* [Implementing the Component](#implementing-the-component)
* [Conclusion](#conclusion)
* [Resources](#resources)


## Setting Up The App 

__Steps:__
* Create a new react app, and `cd` into that directory.
* Jest is installed as a dependency to React when using `npx-create-react-app`, along with the React Testing Library. The React Testing Library provides additional functions to find and interact with DOM nodes of components. No additional installation or setup is needed when beginning your React app this way.

```sh
npx create-react-app jest-react-example
cd jest-react-example
```

## Anatomy of the Default Test

When a new React app is created using `npx-create-react-app`, the `App.js` file comes pre-filled with placeholder content and a test file is included for this by default - `App.test.js`. Let's walk through what happening in this test file:

```js
// App.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});
```
1. We begin by importing two crucial functions from the React Testing Library: `render` and `screen`. 
  * `Render` is a function that will build the DOM tree in memory that would normally be rendered as a webpage. We will use this to turn our component code into the format that the user would be interacting with.
  * `Screen` is a an object with a number of querying functions that will allow us to target element(s) in the DOM. For comparison, it functions similarly to `querySelector`, however the syntax is a bit different since we will not be using an element's tag/class/id.

2. The next import, `userEvent` will allow us to simulate a variety of user actions with a targeted element, such as button presses, typing, etc.[The full documentation for userEvent can be found here](https://testing-library.com/docs/ecosystem-user-event)

3. The third import, `@testing-library/jest-dom/extend-expect`, provides additional matchers that we can use for targeted elements. [The full documentation for Jest-DOM can be found here](https://testing-library.com/docs/ecosystem-jest-dom)

4. Lastly, we need to import the component that we will be testing in this file.

With these imports completed, we see the familiar structure of a Jest test function.
```js
// Copied from above
test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});
```
- The test function is invoked with a `string` argument describing the test, and a callback function with the test content.
- The callback function first creates the DOM tree for the component by rendering the component.
- The `getByText` function of the `screen` object is invoked with a regular expression argument. The `getByText` function will return the first element in the DOM that has text matching the regular expression, which will then save to a variable for later use.
- The callback is completed with the `expect` and matcher statements. In this case, we are simply stating that we expect that our previous query found an element in the document.

If we start the app on the local machine using `npm start` we can see that the specified link text is clearly visible, and the default test should pass.

![Default React Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cmvy4qalvgoh9brdgvdx.png)

We can confirm that the default test is working before we move on to writing our own by running `npm test` in the console.

![Default App.test.js results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4odho1umjxjo3mnfy2a4.png)

## Planning the Tests

Following Test-Driven Development, let's begin by defining what our App _should_ do, write the tests for it, and then implement the code that _should_ pass the tests.

* There will be two buttons: increment and decrement. 
 * When clicked, they should increase/decrease a counter on the page. 
 * The counter should never be negative, so the decrement button should be disabled when the counter is less than 1.

* There should be a form with an input field and a submit button. 
 * The user should be able to type into the field, and when submitted, the text from the field will display in a list on the screen.
 * Each list item will have a "remove" button, that when pressed should remove that item from the screen.

### Describe the Tests

Since the counter value will just be a number, I wanted to ensure that the query matches the counter value and not another number that is potentially on the page (as may happen with just using `getByText()`). For this, we can use the dataset attribute `data-testid` similar to how we use `id` in HTML. The difference is that `data-testid` is strictly for testing purposes and not related to CSS or other interactions.

#### Counter Tests

###### __Test #1:__
In this first test, I wrote the expectation statements to match the initial plan for the counter feature. We expect the DOM to include both buttons, the counter label "Counter: ", and the value of the counter. We would also expect that when the page is first loaded, the counter has a default text value of 0, and because of this, our decrement button should be disabled to not allow a negative counter value.

```js
describe( 'App Counter', () => {
  test('Counter Elements should be present', () => {
    render(<App />)
    const incrementButton = screen.getByText(/Increment/i)
    const decrementButton = screen.getByText(/Decrement/i)
    const counterLabel = screen.getByText(/Counter:/i)
    const counterText = screen.getByTestId("counter-value")
    
    expect(incrementButton).toBeInTheDocument()
    expect(incrementButton).toBeEnabled()
    expect(decrementButton).toBeInTheDocument()
    expect(decrementButton).toBeDisabled()
    expect(counterLabel).toBeInTheDocument()
    expect(counterText).toHaveTextContent(0)
  })
})
```

###### __Test #2__
For the counter, we expect that each time the increment button is pressed, the counter value should increase by 1. When the counter goes above zero, the decrement button should become enabled. To simulate a button press, we use the `click()` function in the `userEvent` object we had imported earlier.

```js
// Within the describe block from test #1
  test('Increment increases value by 1 and enables decrement button present', () => {
    render(<App />)
    const incrementButton = screen.getByText(/Increment/i)
    const decrementButton = screen.getByText(/Decrement/i)
    const counterText = screen.getByTestId("counter-value")

    expect(counterText).toHaveTextContent(0)
    userEvent.click(incrementButton)
    expect(counterText).toHaveTextContent(1)
    expect(decrementButton).not.toBeDisabled()
  })
```js

###### __Test #3__
We expect that when the decrement button is pressed, the counter value should decrease by 1. When the counter reaches zero, the decrement button should become disabled.

```js
// Within the describe block from test #1

  test('Decrement decreases value by 1 and disables decrement button at 0', () => {
    render(<App />)
    const incrementButton = screen.getByText(/Increment/i)
    const decrementButton = screen.getByText(/Decrement/i)
    const counterText = screen.getByTestId("counter-value")

    expect(counterText).toHaveTextContent(0)
    userEvent.click(incrementButton)
    expect(counterText).toHaveTextContent(1)
    expect(decrementButton).not.toBeDisabled()
    userEvent.click(decrementButton)
    expect(counterText).toHaveTextContent(0)
    expect(decrementButton).toBeDisabled()
  })
```

#### Form Tests

The second feature of our mini-app, to explore how we can test for user interaction with a form, involves a form that creates list items when submitted.

###### __Test #4__
First, we can create the basic test to ensure that the expected elements are rendered to the page, similar to what was done earlier.

```js
describe('App Item List', () => {
  test('List Form Components render', () => {
    render(<App />)
    const listItemInput = screen.getByLabelText(/Create List Item/i)
    const addItemButton = screen.getByTestId("add-item")

    expect(listItemInput).toBeInTheDocument()
    expect(addItemButton).toBeInTheDocument()
  })
```
###### __Test #6__
Now that we have confirmed that the elements exist, we need to ensure that they function as expected: 
 - Initially, we would expect the input field to be empty, and that the user should able to type into the field and change the value of the field.
 - With text in the field, we expect that the user should be able to click on the submit button to create a new list item on the page with that text, and it would reset the input field.

```js
  test('User can add item to page', () => {
    render(<App />)
    const listItemInput = screen.getByLabelText(/Create List Item/i)
    const addItemButton = screen.getByTestId("add-item")

    expect(listItemInput).toHaveValue("")
    userEvent.type(listItemInput, "hello")
    expect(listItemInput).toHaveValue("hello")

    userEvent.click(addItemButton)
    expect(screen.getByText("hello")).toBeInTheDocument()
    expect(listItemInput).toHaveValue("")
  })
```
###### __Test #7__
After a list item has been created, the user should be able to click the remove button next to it, to remove it from the page.

```js
  test('User can remove item from page', () => {
    render(<App />)
    const listItemInput = screen.getByLabelText(/Create List Item/i)
    const addItemButton = screen.getByTestId("add-item")

    userEvent.type(listItemInput, "hello")
    userEvent.click(addItemButton)
    const newItem = screen.getByText("hello")
    expect(newItem).toBeInTheDocument()
    
    const removeButton = screen.getByTestId('remove-item0')
    userEvent.click(removeButton)
    expect(newItem).not.toBeInTheDocument()
  })
```

## Implementing the Component

With the tests in place, we should now build our component, and it should meet the expectations set in our tests. Writing the code for the component is no different than it would be without the tests in place. The only additional thing we must do, is include the `data-testid` on the elements for which our tests were querying the elements by using `getByTestId()` such as the list items and buttons. The full code implemented to create the component can be found below the demo.

__End Result__:
![Example App Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ghxinrwnsdrkmayc574w.gif)

We can now run the tests using `npm test` as see the results!

![Final Test Results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pyodezccpxlfh6ifdson.png)

Below is the code used to create the component demonstrated above, using hooks:

```js
import { useState } from 'react'
import './App.css';

function App() {
  const [counter, setCounter] = useState(0)
  const [listItems, setListItems] = useState([])
  const [newItemText, setNewItemText] = useState("")

  const handleCounterClick = value => {
    setCounter( counter => counter + value )
  }

  const handleNewItemChange = e => {
    setNewItemText(e.target.value)
  }

  const handleAddItem = e => {
    e.preventDefault()
    setListItems([...listItems, {
      text: newItemText,id: listItems.length
      }
    ])
    setNewItemText('')
  }

  const handleRemoveItem = id => {
    const newListItems = listItems.filter( item => item.id !== id)
    setListItems(newListItems)
  }

  const listItemComponents = listItems.map( item => {
    return (
      <li
        data-testid={`item${item.id}`}
        key={item.id}
      >
        {item.text}
        <button
          data-testid={`remove-item${item.id}`}
          onClick={() => handleRemoveItem(item.id)}
        >
          Remove
        </button>
      </li>
    )
  })
  return (
    <div className="App">
      <header className="App-header">
        <p>
          Counter:
          <span data-testid="counter-value">
            {counter}
          </span>
        </p>
        <div>
          <button 
            onClick={() => handleCounterClick(1)}
          >
            Increment
          </button>
          <button 
            onClick={() => handleCounterClick(-1)}
            disabled={counter <= 0}
          >
            Decrement
          </button>
        </div>
        <form onSubmit={handleAddItem}>
          <label
            htmlFor="newItem"
          >
            Create List Item
            <input 
              id="newItem"
              value={newItemText}
              onChange={handleNewItemChange}
            />
          </label>
          <input
            data-testid="add-item"
            type="submit"
            value="Add Item"
          />
        </form>
        <ul>
          {listItemComponents}
        </ul>


      </header>
    </div>
  );
}

export default App;


```

## Conclusion:
While this only scratches the surface of testing React components, I hope this serves as a primer for getting started with developing your own tests for your components. 

## Resources:
* [A Beginner's Guide to Unit-testing with Jest](https://dev.to/dsasse07/a-beginner-s-guide-to-unit-testing-with-jest-45cc)
* [Testing-Library/Jest-DOM Documentation](https://www.npmjs.com/package/@testing-library/jest-dom)
* [Testing-Library/React Documentation](https://testing-library.com/docs/)
* [Blog Repo](https://github.com/dsasse07/jest-react-example)