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 18.2.0;
- Jest 28.1.2; e
- Testing Library 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:
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:
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",
};
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:
And if any of the two fields have any invalid data, it is displayed to the user (Invalid email or password
):
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
).
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", () => {})
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
});
})
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 />);
});
})
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" });
});
});
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();
});
});
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 mathcertoBeInDocument
.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);
With the form completed, we will use the click
method to click on the login button.
await userEvent.click(loginButton);
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();
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 ofgetByText
, because it is expected that the element is not in the document, and when we use any method started withget
and no element is found, an error interrupting the test, since the methods started withquery
returnnull
, 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();
});
});
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:
//...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.
Top comments (0)