DEV Community

Diego Chueri
Diego Chueri

Posted on • Edited on

[React + Jest] Introducing to Components Tests

Versão em português

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:

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Resulting in the following interface:

Print screen

Understand how LoginForm works

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

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

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

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

Print screen

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

Starting, let's use the describe 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:

describe("LoginForm", () => {})
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

describe("LoginForm", () => {
  it("should render the login form", () => {
    render(<LoginForm />);
  });
})
Enter fullscreen mode Exit fullscreen mode

Capturing an element on DOM

The screen is used to query the DOM. It has a series of queries that will query the DOM as needed and return the chosen element.

//...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" });
  });
});
Enter fullscreen mode Exit fullscreen mode

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.

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 that perform checks between the expected value and the received value.

//...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();
  });
});
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

💡 Don't forget to import the @testing-library/jest-dom module to use mathcer toBeInDocument.

See more about Testing Library 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.

await userEvent.type(emailField, userInput.email);
await userEvent.type(passwordField, userInput.password);
Enter fullscreen mode Exit fullscreen mode

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

await userEvent.click(loginButton);
Enter fullscreen mode Exit fullscreen mode

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

// 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();
Enter fullscreen mode Exit fullscreen mode

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:

//...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();
  });
});
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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:

//...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();
  });
});
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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

Read too

ArchLinux config on WSL2 to developers

Top comments (0)