[React + Jest] Introduzindo Testes para "Components"
Tags: react, frontend, teste, jest
Published: 2022-12-09 22:03:49

> [English version](https://dev.to/dchueri/react-jest-introducing-to-components-tests-9d9)

## 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](https://pt-br.reactjs.org/) 18.2.0;
- [Jest](https://jestjs.io/pt-BR/) 28.1.2; e
- [Testing Library](https://testing-library.com/) 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:

```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>
  );
};
```

Resultando na seguinte interface:

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

### Entendendo o funcionamento

Foi utilizado como [mock](https://pt.wikipedia.org/wiki/Objeto_mock) de um usuário no banco de dados a constante `db`:

```tsx
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!`):

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

E caso algum dos dois campos possuam algum dado inválido é exibido para o usuário (`Invalid email or password`):

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

## 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`).

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

Iniciando, vamos utilizar o método [describe](https://jestjs.io/pt-BR/docs/api#describename-fn), 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:

```tsx
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.

```tsx
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.

```tsx
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](https://testing-library.com/docs/queries/about/) que farão a consulta no DOM conforme a necessidade e retornando o elemento escolhido.

```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" });
  });
});
```

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](https://testing-library.com/docs/queries/about/).

## 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*](https://jestjs.io/docs/expect) que realizam verificações entre o valor esperado e o valor recebido.

```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();
  });
});
```

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 *mathcer* `toBeInDocument`.
>
> Veja mais sobre os Testing Library [Custom Matchers](https://github.com/testing-library/jest-dom#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.

```tsx
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.

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

Agora podemos realizar a comparação entre os valores esperados e os valores recebidos:

```tsx
// 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 o `queryByText` no lugar de `getByText`, isso por conta de que é esperado que o elemento não esteja no documento, e ao utilizarmos qualquer método iniciado com `get` e não for encontrado nenhum elemento será lançado um erro interrompendo o teste, já os métodos iniciados com `query` retornam `null` , possibilitando a continuação do teste.

Fincando dessa forma o nosso segundo teste:

```tsx
//...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:

```tsx
//...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
[React Tests - GitHub](https://github.com/dchueri/react-tests)

### Leia também
[Configurando o ArchLinux com WSL 2 para Devs](https://dev.to/dchueri/configurando-o-archlinux-com-wsl-2-para-devs-393n)