Unit tests in React with Jest and Testing Library
Tags: react, jest, testinglibrary, webdev
Published: 2024-04-29 21:23:51

## Introduction

In the [article](https://dev.to/griseduardo/setup-jest-babel-e-testing-library-for-unit-testing-in-react-4pim) from March, I wrote about how to set up Jest, Babel, and Testing Library to perform unit tests in React. The idea now is to show how the testing process works, with some concepts and examples.

## Jest Testing Structure

The testing structure will follow:

- describe: represents the block of tests to be executed, which could be a block of tests related to a specific component, for example
- it: represents the test to be executed, where the component will be rendered, will be searched an HTML element inside it, and will be simulated an user interaction
- expect: performs test validation, comparing the expected result with the test result

```
describe("<Component />", () => {
  it("should…", () => {
    render component
    search component element that will be tested
    user interaction

    expect().matcher()
  })

  it("should…", () => {
      …
  })
})
```

## testing-library/react

The library that will allow component rendering in tests and HTML element search after rendering the component.

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

- render: render the component
- screen: allow search element after component render

The element search is done using queries. Below are the available types:

| Type | 0 matches | 1 match | Multiples matches | Retry |
| :--: | :-------: | :-----: | :---------------: | :---: |
| getBy | Returns error | Returns element | Returns error | No |
| queryBy | Returns null | Returns element | Returns error | No |
| findBy | Returns error | Returns element | Returns error | Yes |
| getAllBy | Returns error | Returns array | Returns array | No |
| queryAllBy | Returns [ ] | Returns array | Returns array | No |
| findAllBy | Returns error | Returns array | Returns array | Yes |

- Returns error: causes the test to fail at the element search stage (does not proceed with the test)
- Returns element: returns the element that satisfied the search
- Returns null: returns null if no element satisfied the search (does not break the test, allows do an validation based on this information)
- Returns array: returns an array with all elements that satisfy the search
- Returns [ ]: returns an empty array if no element satisfied the search (does not break the test, allows do an validation based on this information)

Here are some examples of search using `getBy` as base:

| getBy | Search | Code |
| :---: | :---: | :----: |
| getByRole | by what represents | getByRole(searchedRole, {name: name}) |
| getByText | by text | getByText(text) |
| getByTestId | by test id | getByTestId(testId) |
| getByLabelText | by label text | getByLabelText(labelText, selector) |

## Jest matchers

The Jest provides some `matchers` for test validation. I'll list some below based on the examples of tests that will be performed:

| Matcher | Validation |
| :-----: | :-------: |
| toBe(value) | value |
| toHaveLength(number) | array or string length |
| toHaveBeenCalledTimes(number) | number of calls |

## testing-library/jest-dom

Provides additional matchers in addition to those already present in Jest:

``import "@testing-library/jest-dom"``

Folow some examples:

| Matcher | Validation |
| :-----: | :-------: |
| toBeInTheDocument() | element presence |
| toBeDisabled() | disabled element |
| toHaveTextContent(text) | text content |

## Initial tests examples


In this first example, we'll have a button component that, through the `clickedNumber` constant, records the number of clicks on it via the `onClick` function. If the number of clicks is greater than zero, it displays the click count on the screen. Additionally, it accepts a `disabled` props, which, once passed, disables the button:

```js
import React, { useState } from "react";

const BaseComponent = ({ disabled }) => {
  const [clickedNumber, setClickedNumber] = useState(0);

  const onClick = () => {
    setClickedNumber(clickedNumber + 1);
  };

  return (
    <>
      <button 
        data-testid="baseButton" 
        onClick={onClick} 
        disabled={disabled}
      >
        Activations
      </button>
      {clickedNumber > 0 
        && <p data-testid="baseParagraph">{clickedNumber}</p>}
    </>
  );
};

```

In the test block below, two things will be validated:

- Test 1: if the button is present after rendering the component
- Test 2: if the paragraph displaying the click count is not present (since the button has not been clicked)

```js
import React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";

import BaseComponent from "./BaseComponent";

describe("<BaseComponent />", () => {
  beforeEach(() => {
    render(<BaseComponent />);
  });

  it("should bring button element", () => {
    const element = screen.getByRole("button", { name: "Activations" });
    // const element = screen.getByText("Activations");
    // const element = screen.getByTestId("baseButton");

    expect(element).toBeInTheDocument();
  });

  it("should not bring paragraph element", () => {
    const element = screen.queryByTestId("baseParagraph");

    expect(element).not.toBeInTheDocument();
  });
});

```

In both tests, the component will be rendered in the same way, so a `beforeEach` is placed before them with the rendering.

In the first test, three different ways of finding the button are showed: by its role (searching for the role `button` and its name text `Activations`), by its text directly and by its test ID (corresponding to the data-testid present in the component). Finally, it's validated if the button is present using the `toBeInTheDocument()` matcher.

The second test checks for the absence of the paragraph since the button hasn't been clicked. In this case, instead of using `getBy`, `queryBy` is used because `getBy` would break the test if it couldn't find the element. However, the purpose of the test is to verify the absence of the element. Using `queryBy`, the test doesn't break (as it returns `null` for the search) and the absence can be verified by negating the `toBeInTheDocument()` matcher.

After executing the tests, they pass successfully:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mpm1q2w1hceup3xvcip2.png)

From top to bottom, it is possible to observe:

- file that was executed: `src/BaseComponent.test.js`
- block that was executed: `<BaseComponent />`
- tests that were executed: both tests with their descriptions and a positive checkmark on the left indicating they passed
- Test Suites: the number of test blocks executed and how many passed
- Tests: the number of tests executed and how many passed

To illustrate a failure result, the search for the paragraph in the second test was modified to use `getBy`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5vp37ycn2qsh7fyt86gk.png)


In addition to the information provided above, in case of failure:

- The failed test is indicated with an x to the left of it
- General information about the failed test is displayed in red, including the description of the block and test that failed. Below that, how the rendered component was at the time of failure, followed by the line of code where the test failed
- Test Suites: Out of a total of one block, it indicates that one failed. This is because a test block fails if any test inside it fails
- Tests: Out of two tests, one passed and one failed

Now, to test if the button will be disabled or not, two tests will be conducted by rendering the component in two different ways: passing or not passing the `disabled` props:

- Test 1: rendering the `BaseComponent` without passing the `disabled` props
- Test 2: rendering the `BaseComponent` passing the `disabled` props

```js
import React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";

import BaseComponent from "./BaseComponent";

describe("<BaseComponent />", () => {
  it("should bring button element enabled", () => {
    render(<BaseComponent />);

    const element = screen.getByRole("button", { name: "Activations" });

    expect(element).not.toBeDisabled();
  });

  it("should bring button element disabled with disabled props", () => {
    render(<BaseComponent disabled />);

    const element = screen.getByRole("button", { name: "Activations" });

    expect(element).toBeDisabled();
  });
});

```


In both tests, the `toBeDisabled()` matcher was used. The first test aimed to validate that the button remains enabled since the `disabled` props was not passed, negating the matcher in the validation. The second test aimed to validate that the button disables when the `disabled` props was passed to the component.

## testing-library/user-event

The library that will allows simulate the user's interaction with the component.

``import userEvent from "@testing-library/user-event"``

| User event | Action |
| :--------: | :----: |
| click(), dblClick() | click, double click |
| selectOptions() | option selection |
| paste() | text paste |
| type() | text write |
| upload() | file upload |

## Interaction test example

In this example, the same component from the initial examples above will be used, but with another validations:

- Test 1: validate the appearance of the paragraph displaying the click count after one button click, ensuring it displays the value 1
- Test 2: validate the appearance of the paragraph displaying the click count after a double click on the button, ensuring it displays the value 2

```js
import React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import BaseComponent from "./BaseComponent";

describe("<BaseComponent />", () => {
  beforeEach(() => {
    render(<BaseComponent />);
  });

  it("should bring paragraph with clicked quantity after button click", () => {
    const buttonElement = screen.getByRole("button", { name: "Activations" });

    userEvent.click(buttonElement);

    const paragraphElement = screen.queryByTestId("baseParagraph");

    expect(paragraphElement).toBeInTheDocument();
    expect(paragraphElement).toHaveTextContent(1);
  });

  it("should bring paragraph with clicked quantity after double button click", () => {
    const buttonElement = screen.getByRole("button", { name: "Activations" });

    userEvent.dblClick(buttonElement);

    const paragraphElement = screen.queryByTestId("baseParagraph");

    expect(paragraphElement).toBeInTheDocument();
    expect(paragraphElement).toHaveTextContent(2);
  });
});

```

In the first test, one button click is simulated using the user event `click`. After this click, validation is performed to ensure the appearance of the paragraph using the `toBeInTheDocument()` matcher, and the displayed click count is validated using the `toHaveTextContent()` matcher. In the second test, similar validations are performed using the same matchers, but with the expected click count value inside the `toHaveTextContent()` matcher. To simulate the double click, the user event `dblClick` is used.

## Functions mock

It allows mocking functions present inside the component, setState(), requests.

``const mockFunction = jest.fn()``

| Analysis | Code |
| :-----: | :----: |
| Function calls | mockFunction.mock.calls |
| Variables passed in calls | mockFunction.mock.calls[0][0] |
| Call result | mockFunction.mock.results[0].value |
| Clear all mocks | jest.clearAllMocks() |

- jest.clearAllMocks(): it is used to clear the mock function between tests because the function calls are not automatically cleared (they accumulate throughout the tests if not cleaned)
- mockFunction.mock.calls: the first [ ] corresponds to which call of the function is being analyzed, and the second [ ] corresponds to which variable of that call is being analyzed

For example, considering a function f(x, y) that was called twice during the tests:

- mockFunction.mock.calls[0][0]: value of x in the first call
- mockFunction.mock.calls[0][1]: value of y in the first call
- mockFunction.mock.calls[1][0]: value of x in the second call
- mockFunction.mock.calls[1][1]: value of y in the second call

## Mock test example

To perform mock tests, a new component will be used. It's an input field with a label `Value:`, which receives a setState via props called `setValue`. Every time the text inside the input is modified, it triggers a function `handleChange`, which calls `setValue` passing the current value present in the input field:

```js
import React from "react";

const BaseComponent = ({ setValue }) => {
  const handleChange = (e) => {
    setValue(e.target.value);
  };

  return (
    <label>
      Value:
      <input type="text" onChange={(e) => handleChange(e)} />
    </label>
  );
};

export default BaseComponent;

```

Two tests will be performed, mocking the `setValue` to validate how many times it's called and the value passed to it:

- Test 1: 10 will be typed into the input field using the userEvent `type`, which corresponds to two changes in the input field (since typing 10 involves typing 1 and then 0)
- Test 2: 10 will be pasted into the input field using the userEvent `paste`, which corresponds to one change in the input field

```js
import React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import BaseComponent from "./BaseComponent";

const setValue = jest.fn();

describe("<BaseComponent />", () => {
  beforeEach(() => {
    render(<BaseComponent setValue={setValue} />);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it("should call setValue after type on input", () => {
    const element = screen.getByLabelText("Value:", { selector: "input" });

    userEvent.type(element, "10");

    expect(setValue).toHaveBeenCalledTimes(2);
    // expect(setValue.mock.calls).toHaveLength(2);
    expect(setValue.mock.calls[0][0]).toBe("1");
    expect(setValue.mock.calls[1][0]).toBe("10");
  });

  it("should call setValue after paste on input", () => {
    const element = screen.getByLabelText("Value:", { selector: "input" });

    userEvent.paste(element, "10");

    expect(setValue).toHaveBeenCalledTimes(1);
    expect(setValue.mock.calls[0][0]).toBe("10");
  });
});

```

At the beginning of the test, `setValue` is mocked using `jest.fn()`. Since both tests render the component in the same way, rendering is placed inside a `beforeEach`. Due to the mock function accumulating calls made to it and not being automatically cleared between tests, calls are cleared after each test execution with `jest.clearAllMocks()` in the `afterEach`.
The input field is seached by its label `Value:` and specifying the selector type `input`.
In the first test, which involves typing, the user event `type` is used to input the value 10. Two ways are used to validate the number of calls to `setValue`: directly checking the number of times the mock function was called using the matcher `toHaveBeenCalledTimes()`, and checking the length of the array recording the calls (setValue.mock.calls) using the matcher `toHaveLength()`. Finally, the value passed to `setValue` is checked using `setValue.mock.calls[0][0]` for the first call and `setValue.mock.calls[1][0]` for the second call.
The second test performs the same validations, but instead of typing 10, the number is pasted directly using the user event `paste`.

## Conclusion

The idea was to provide a general overview of how unit tests work using Jest with testing library, covering test structure, component rendering, element search inside the rendered component, user interaction simulation, function mocking and test validations. However, the use of these libraries allows various other types of tests to be conducted, which is why I'm providing the main links separated by themes for those who want to delve deeper.

## Links

[Tests structure](https://jestjs.io/docs/api)
[Queries](https://testing-library.com/docs/queries/about/)
[Roles](https://www.w3.org/TR/html-aria/#docconformance)
[User events](https://testing-library.com/docs/user-event/intro/)
[Matchers Jest](https://jestjs.io/docs/expect)
[Matchers testing-library](https://github.com/testing-library/jest-dom)
[Mock functions](https://jestjs.io/docs/mock-functions)