Diego Chueri
Posted on December 9, 2022
Introdução
Diferente do Backend onde os testes unitários visam avaliar o funcionamento de um determinado método, os testes unitários para o Frontend devem focar em "simular" as possíveis ações dos usuários e como determinado componente reage a determinada ação.
Porque escrever testes?
Embora possa parecer complicado no início, testes automatizados podem evitar dor de cabeça no futuro. São a forma mais efetiva de verificar se o seu código realmente faz o que você deseja que ele faça, podem evitar que algum bug que já tenha sido corrigido previamente volte a ocorrer, e diversos outros benefícios para a sua aplicação.
Tecnologias
Para escrever nossos testes iremos utilizar a stack a seguir:
- React 18.2.0;
- Jest 28.1.2; e
- Testing Library 13.4.0.
"Component" a ser testado
Para escrever um bom teste você deve conhecer bem o código e o comportamento que espera, então vamos analisar o nosso LoginForm antes de iniciar os testes:
Apresentando o Código
Como exemplo, será utilizado um component simples de um formulário de login:
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>
);
};
Resultando na seguinte interface:
Entendendo o funcionamento
Foi utilizado como mock de um usuário no banco de dados a constante db
:
const db = {
email: "diego@test.com",
password: "123456",
};
Em seguida está o nosso LoginForm. Basicamente ele armazena o valor do input de cada campo do formulário no state por meio do onChange
. Ao clicar no botão de Log In é acionado o handleSubmit
que por meio do método de login
faz a verificação se o e-mail e senha passados pelo usuário coincidem com a senha do usuário no banco (no nosso caso, no mock).
Caso o e-mail e senha sejam válidos, é exibido uma mensagem de boas vindas (Welcome!
):
E caso algum dos dois campos possuam algum dado inválido é exibido para o usuário (Invalid email or password
):
Iniciando os testes
Vamos criar o arquivo de testes para o nosso component na mesma pasta em que ele está inserido. Por boas práticas, costumo nomear o arquivo de testes com o mesmo nome do component, seguido da extensão .spec.tsx
(caso não esteja utilizando typescript como eu a extensão seria .spec.jsx
).
Iniciando, vamos utilizar o método describe, onde passaremos como primeiro parâmetro uma string que normalmente é o nome do component testado. O segundo parâmetro é uma função callback que receberá nossos testes:
describe("LoginForm", () => {})
Os testes são escritos dentro das funções it
ou test
, os dois são funcionalmente idênticos, sendo que prefiro utilizar o it
apenas por semântica, melhorando a legibilidade. A função também recebe dois parâmetros, onde o primeiro é a descrição do que deverá acontecer durante o teste e o segundo é mais um callback, mas dessa vez com o código do teste.
describe("LoginForm", () => { // LoginForm...
it("should render the login form", () => { // ...deve renderizar o formulário de login
//code for test here
});
})
Neste momento podemos começar testando a renderização dos elementos.
Renderizando a UI
O método render()
realizará a renderização do componente. Ele recebe como parâmetro o componente e retorna um RenderResult que tem vários métodos e propriedades utilitários.
import { render } from "@testing-library/react";
import { LoginForm } from "../LoginForm";
describe("LoginForm", () => {
it("should render the login form", () => {
render(<LoginForm />);
});
})
Capturando elementos no DOM
O objeto screen
é utilizado para fazer consultas no DOM. Ele possui uma série de queries que farão a consulta no DOM conforme a necessidade e retornando o elemento escolhido.
//...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" });
});
});
Foram apresentados três exemplos de queries dentre as diversas que podem ser utilizadas:
-
getByLabelText
: Recomendado pela documentação oficial para buscar campos de formulários (no exemplo não utilizei esta query em todas as opções para ampliar a apresentação das possibilidades de uso); -
getByText
: Fora dos formulários, o conteúdo de texto é a principal forma de encontrar elementos. Este método é recomendado para localizar elementos não interativos (como divs, spans e parágrafos). -
getByRole
: Utilizado para consultar elementos pelo nome acessível dele (button, div, p, entre outros). Normalmente é utilizando com o objeto option {name: …}, evitando elementos indesejados.
Sugiro a leitura documentação com todas as queries neste link.
Hora de testar
Testando a renderização
Vamos agora finalmente testar se os elementos estão sendo renderizados. Para testar os valores é utilizada a função expect
, ela utiliza funções matchers que realizam verificações entre o valor esperado e o valor recebido.
//...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();
});
});
Rodando o teste com os comandos abaixo, poderemos observar o resultado no terminal.
yarn test
ou
npm run test
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
💡 Não se esqueça de importar o módulo
@testing-library/jest-dom
para utilizar o mathcertoBeInDocument
.Veja mais sobre os Testing Library Custom Matchers.
Mais testes
Agora que sabemos que os elementos esperados estão sendo renderizados normalmente, iremos verificar o comportamento deles diante uma interação do usuário, e para isso utilizaremos o userEvent
.
Testando o login bem sucedido
Nesse momento relembro que é necessário imaginar as ações que o usuário irá executar. Por tanto, para realizar o login o usuário primeiro insere os dados nos campos de email
e password
, por último ele clica no botão de Log In
recebendo então a mensagem de boas vindas caso os dados estejam corretos ou, caso contrário, receberá uma mensagem de erro.
O método type
emula a digitação pelo usuário, recebendo como primeiro parâmetro o elemento no qual será inserido o texto e como segundo parâmetro o texto propriamente dito. Ele retorna uma Promise
portanto é necessário adicionar o await
antes do userEvent e o async
no início da função callback do teste.
await userEvent.type(emailField, userInput.email);
await userEvent.type(passwordField, userInput.password);
Com o formulário preenchido, utilizaremos o método click
para realizar o clique no botão de login.
await userEvent.click(loginButton);
Agora podemos realizar a comparação entre os valores esperados e os valores recebidos:
// espera que o elemento com o texto "Welcome!" esteja no documento
expect(screen.getByText("Welcome!")).toBeInTheDocument();
// espera que o elemento com o texto "Invalid email or password" não seja encontrado, retornando null
expect(screen.queryByText("Invalid email or password")).toBeNull();
Observe que no primeiro expect é esperado que um elemento com o texto “Welcome!” esteja no documento, utilizando o já conhecido método
getByText
. Já no segundo foi utilizado oqueryByText
no lugar degetByText
, isso por conta de que é esperado que o elemento não esteja no documento, e ao utilizarmos qualquer método iniciado comget
e não for encontrado nenhum elemento será lançado um erro interrompendo o teste, já os métodos iniciados comquery
retornamnull
, possibilitando a continuação do teste.
Fincando dessa forma o nosso segundo teste:
//...imports
import userEvent from "@testing-library/user-event";
describe("LoginForm", () => {
it("should render the login form", () => {
...
});
// deve exibir uma mensagem de boas vindas caso tenha sucesso no login
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 an 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
Testando o login mal sucedido
O último teste que vamos escrever neste artigo é para caso o e-mail ou a senha inseridos pelo usuário sejam inválidos. A lógica será basicamente a mesma do teste anterior alternado somente a ordem das mensagens esperadas:
//...imports
describe("LoginForm", () => {
it("should render the login form", () => {
...
});
it("should show an welcome message if login success", async () => {
...
});
// deve exibir uma mensagem de erro caso o login tenha falhado
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 an 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
Conclusão
Busquei neste artigo principalmente abrir a porta para o universo dos testes. Uma ideia para exercitar o que aprendeu aqui é clonando o repositório deste projeto e criar outros testes, como por exemplo testar o que ocorrerá caso o usuário não preencha algum dos campos quando houver um required
.
Com este artigo, assim como com qualquer outro artigo que você ler, busque não se restringir somente ao que foi demonstrado, utilize os links auxiliares disponibilizados ao decorrer da explicação, explore, exercite e conheça melhor cada funcionalidade para aprimorar seu conhecimento.
Repositório do Projeto
Leia também
Posted on December 9, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.