[React + Jest] Introducing to Components Tests
Tags: frontend, test, react, beginners
Published: 2022-12-10 13:40:25

> [Versão em português](https://dev.to/dchueri/react-jest-introduzindo-testes-para-components-3ao3)

## Introduction
Different from the Backend where the unit tests aim to evaluate the behavior of a certain method, the unit tests for the Frontend must focus on "simulating" the possible actions of the users and how a certain component reacts to a certain action.

## Why write tests?
Although it may seem complicated at first, automated tests can save you headaches in the future. They are the most effective way to verify that your code does what you want it to do, can prevent a bug that has already been fixed from occurring again, and more others benefit your application.

## Technologies
To write our tests, we will use the following stack:

- [React](https://reactjs.org/) 18.2.0;
- [Jest](https://jestjs.io/) 28.1.2; e
- [Testing Library](https://testing-library.com/) 13.4.0.

## Component to be tested
To write a good test you must know the code and the behavior you expect, so let's analyze our **LoginForm** before starting the tests.

### Introducing the Code

As an example, a simple *component* of a login form will be used:

```tsx
import { useState } from "react";

const db = {
  email: "diego@test.com",
  password: "123456",
};

export const LoginForm = () => {
  const [email, setEmail] = useState<string>();
  const [password, setPassword] = useState<string>();
  const [logged, setLogged] = useState<Boolean>(false);
  const [error, setError] = useState<Boolean>(false);

  const login = (email: string, password: string) => {
    if (email !== db.email || password !== db.password) {
      setError(true);
      setLogged(false);
    } else {
      setError(false);
      setLogged(true);
    }
  };

  const handleSubmit = (e: React.SyntheticEvent) => {
    e.preventDefault();
    login(email!, password!);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email
        <input
          name="email"
          type="text"
          onChange={(e) => setEmail(e.target.value)}
        />
      </label>
      <label>
        Password
        <input
          name="password"
          type="password"
          onChange={(e) => setPassword(e.target.value)}
        />
      </label>
      <button type="submit">Log in</button>
      {logged ? (
        <p>Wellcome!</p>
      ) : error ? (
        <p>Email or password invalid</p>
      ) : null}
    </form>
  );
};
```

Resulting in the following interface:

![Print screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rrd6cirar3vvkakn5pym.png)

### Understand how LoginForm works

Was used the `db` constant as a mock of a user on the database:

```tsx
const db = {
  email: "diego@test.com",
  password: "123456",
};
```

Next is our **LoginForm**. Basically, it stores the input value of each form field in *state* through `onChange`. When clicking on the **Log In** button, the `handleSubmit` is activated, which, through the `login` method, verifies whether the e-mail and password passed by the user coincide with the user's password in the bank (in our case, in the mock). 

If the email and password are valid, a welcome message (`Welcome!`) is displayed:

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

And if any of the two fields have any invalid data, it is displayed to the user (`Invalid email or password`):

![Print screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ud92n9em7i52e5kr4rd.png)

## Starting the tests

Let's create the test file for our *component* in the same folder where it is inserted. For good practices, I usually name the test file with the same name as the *component*, followed by the `.spec.tsx` extension (if you are not using typescript like me, the extension would be `.spec.jsx`).

![Print screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg6do7uc3lnc7s7cstlu.png)

Starting, let's use the [describe](https://jestjs.io/docs/api#describename-fn) method, where we'll pass a string as the first parameter, which is usually the name of the tested component. The second parameter is a callback function that will receive our tests:

```tsx
describe("LoginForm", () => {})
```

Tests are written inside the `it` or `test` functions, the two are functionally identical, and I prefer to use `it` just for semantics, improving readability. The function also receives two parameters, where the first is a description of what should happen during the test and the second is another callback, but this time with the test code.

```tsx
describe("LoginForm", () => {                // LoginForm...
	it("should render the login form", () => { // ...deve renderizar o formulário de login
		//code for test here
  });
})
```

At this point, we can start testing the rendering of the elements.

## Rendering UI

The `render()` method will render the component. It takes the component as a parameter and returns a *RenderResult* that has several utility methods and properties.

```tsx
import { render } from "@testing-library/react";
import { LoginForm } from "../LoginForm";

describe("LoginForm", () => {
  it("should render the login form", () => {
    render(<LoginForm />);
  });
})
```

## Capturing an element on DOM

The `screen` is used to query the DOM. It has a series of [queries](https://testing-library.com/docs/queries/about/) that will query the DOM as needed and return the chosen element.

```tsx
//...imports
import { render, screen } from "@testing-library/react";

describe("LoginForm", () => {
  it("should render the login form", () => {
    render(<LoginForm />);

		const emailField = screen.getByLabelText("Email");
		const passwordField = screen.getByText("Password");
		const loginButton = screen.getByRole("button", { name: "Log in" });
  });
});
```

Three examples of queries were presented among the many that can be used:

- `getByLabelText`: Recommended by the official documentation to search for form fields (in the example I did not use this query in all options to expand the presentation of the possibilities of use);
- `getByText`: Outside of forms, text content is the main way to find elements. This method is recommended for locating non-interactive elements (such as divs, spans, and paragraphs); and
- `getByRole`: Used to query elements by their accessible name (*button*, *div*, *p*, among others). It is normally used with the *option* {*name: …}* object, avoiding unwanted elements.

I suggest reading the documentation with all queries [in this link](https://testing-library.com/docs/queries/about/).

## Time to test

### Render test
Now let's finally test if the elements are being rendered. To test the values, the `expect` function is used, it uses functions [*matchers*](https://jestjs.io/docs/expect) that perform checks between the expected value and the received value.

```tsx
//...imports
import '@testing-library/jest-dom';

describe("LoginForm", () => {
  it("should render the login form", () => {
    render(<LoginForm />);

		const emailField = screen.getByLabelText("Email");
		const passwordField = screen.getByText("Password");
		const loginButton = screen.getByRole("button", { name: "Log in" });

		expect(emailField).toBeInTheDocument();
    expect(passwordField).toBeInTheDocument();
    expect(loginButton).toBeInTheDocument();
  });
});
```

```
PASS  src/components/LoginForm/LoginForm.spec.tsx
  LoginForm
    ✓ should render the login form (82 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.01 s, estimated 4 s
```

> 💡 Don't forget to import the `@testing-library/jest-dom` module to use *mathcer* `toBeInDocument`.
>
> See more about Testing Library [Custom Matchers](https://github.com/testing-library/jest-dom#custom-matchers).

### More tests

Now that we know that the expected elements are being rendered normally, we will check their behavior when interacting with the user, and for that we will use the `userEvent`.

#### Successful login test

At this point, remember that it is necessary to imagine the user's actions. Therefore, to perform the login, the user first enters the data in the `email` and `password` fields, finally he clicks on the `Log In` button, receiving the welcome message if the data are correct or, if otherwise, you will receive an error message.

The `type` method emulates typing by the user, receiving the element into which the text will be inserted as the first parameter and the text itself as the second parameter. It returns a `Promise` so it is necessary to add the `await` before the *userEvent* and the `async` at the beginning of the test's callback function.

```tsx
await userEvent.type(emailField, userInput.email);
await userEvent.type(passwordField, userInput.password);
```

With the form completed, we will use the `click` method to click on the login button.

```tsx
await userEvent.click(loginButton);
```

Now we can perform the comparison between the expected values and the received values:

```tsx
// expects the element with the text "Welcome!" to be in the document
expect(screen.getByText("Welcome!")).toBeInTheDocument();
// expects that the element with the text "Invalid email or password" is not found, returning null
expect(screen.queryByText("Invalid email or password")).toBeNull();
```

> Note that in the first *expect* it is expected that an element with the text *“Welcome!”* is in the document, using the already known `getByText` method. In the second, `queryByText` was used instead of `getByText`, because it is expected that the element is not in the document, and when we use any method started with `get` and no element is found, an error interrupting the test, since the methods started with `query` return `null` , allowing the continuation of the test.

Staying our second test in this way:

```tsx
//...imports
import userEvent from "@testing-library/user-event";

describe("LoginForm", () => {
  it("should render the login form", () => {
    ...
  });

  it("should show an welcome message if login success", async () => {
    render(<LoginForm />);

    const emailField = screen.getByLabelText("Email");
    const passwordField = screen.getByText("Password");
    const loginButton = screen.getByRole("button", { name: "Log in" });

    const userInput = { email: "diego@test.com", password: "123456" };

    await userEvent.type(emailField, userInput.email);
    await userEvent.type(passwordField, userInput.password);
    await userEvent.click(loginButton);

    expect(screen.getByText("Welcome!")).toBeInTheDocument();
    expect(screen.queryByText("Invalid email or password")).toBeNull();
  });
});
```

```
PASS  src/components/LoginForm/LoginForm.spec.tsx
  LoginForm
    ✓ should render the login form (109 ms)
    ✓ should show a welcome message if login success (141 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.6 s, estimated 3 s
```

#### Failed login test

The last test we are going to write in this article is in case the email or password entered by the user is invalid. The logic will be the same as in the previous test, only changing the order of the expected messages:

```tsx
//...imports

describe("LoginForm", () => {
  it("should render the login form", () => {
    ...
  });

  it("should show an welcome message if login success", async () => {
    ...
  });

  it("should show an error message if login failed", async () => {
    render(<LoginForm />);

    const emailField = screen.getByLabelText("Email");
    const passwordField = screen.getByText("Password");
    const loginButton = screen.getByRole("button", { name: "Log in" });

    const userInput = { email: "diego@test.com", password: "worngPassword" };

    await userEvent.type(emailField, userInput.email);
    await userEvent.type(passwordField, userInput.password);
    await userEvent.click(loginButton);

    expect(screen.getByText("Invalid email or password")).toBeInTheDocument();
    expect(screen.queryByText("Welcome!")).toBeNull();
  });
});
```

```
PASS  src/components/LoginForm/LoginForm.spec.tsx
  LoginForm
    ✓ should render the login form (89 ms)
    ✓ should show a welcome message if login success (166 ms)
    ✓ should show an error message if login failed (126 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        2.388 s, estimated 3 s
```

## Conclusion

I sought in this article mainly to open the door to the universe of tests. An idea to exercise what you've learned here is to clone this project's repository and create other tests, such as testing what will happen if the user doesn't fill in any of the fields when there is a `required`.

With this article, as with any other article you read, **try not to restrict yourself only to what has been demonstrated**, use the auxiliary links made available during the explanation, explore, exercise, and get to know each functionality better to improve your knowledge.

### Project Repository
[React Tests - GitHub](https://github.com/dchueri/react-tests)

### Read too
[ArchLinux config on WSL2 to developers](https://dev.to/dchueri/archlinux-config-on-wsl2-to-developers-48lm)