Testes unitários em React com Jest e testing library
Tags: react, jest, testinglibrary, webdev
Published: 2024-04-29 21:23:35

## Introdução

No [artigo](https://dev.to/griseduardo/setup-jest-babel-e-testing-library-para-testes-unitarios-em-react-7l6) do mês de março, escrevi sobre como configurar Jest, Babel e testing library para ser possível realizar testes unitários em React. A ideia agora é mostrar como funciona a realização dos testes, com alguns conceitos e exemplos.

## Estrutura testes em Jest

A estruturação dos testes vai seguir da seguinte forma:

- describe: vai representar o bloco de testes a ser executado, podendo ser um bloco de testes referente a um componente específico por exemplo
- it: vai representar o teste que vai ser executado, onde será realizada a renderização do componente, a busca de elemento HTML dentro dele e simulada a interação do usuário com ele
- expect: vai realizar a validação dos testes, onde é comparado o que é esperado e o resultado obtido do teste

```
describe("<Component />", () => {
  it("should…", () => {
    renderizar componente
    busca de elemento do componente a ser testado
    interação do usuário

    expect().matcher()
  })

  it("should…", () => {
      …
  })
})
```

## testing-library/react

Lib que vai possibilitar a renderização de componente nos testes e busca de elemento HTML após renderização.

``import { render, screen } from "@testing-library/react"``

- render: renderiza o componente
- screen: após a renderização permite buscar elemento dentro dele

A busca de elemento se dá a partir do uso de queries, segue abaixo os tipos disponíveis:

| Tipo | 0 matches | 1 match | Múltiplos matches | Retentativa |
| :--: | :-------: | :-----: | :---------------: | :-------------------: |
| getBy | Retorna erro | Retorna elemento | Retorna erro | Não |
| queryBy | Retorna null | Retorna elemento | Retorna erro | Não |
| findBy | Retorna erro | Retorna elemento | Retorna erro | Sim |
| getAllBy | Retorna erro | Retorna array | Retorna array | Não |
| queryAllBy | Retorna [ ] | Retorna array | Retorna array | Não |
| findAllBy | Retorna erro | Retorna array | Retorna array | Sim |

- Retorna erro: faz o teste falhar na própria busca do elemento (não avança o teste)
- Retorna elemento: retorna o elemento que satisfez a busca
- Retorna null: retorna null se nenhum elemento satisfez a busca (sem quebrar o teste, permite fazer validação em cima dessa informação)
- Retorna array: retorna array com todos os elementos que satisfazem a busca
- Retorna [ ]: retorna array vazio se nenhum elemento satisfez a busca (sem quebrar o teste, permite fazer validação em cima dessa informação)

Segue abaixo alguns exemplos de forma de busca, usando o `getBy` como base:

| getBy | Busca | Código |
| :---: | :---: | :----: |
| getByRole | pelo que representa | getByRole(searchedRole, {name: name}) |
| getByText | pelo texto | getByText(text) |
| getByTestId | pelo id de teste | getByTestId(testId) |
| getByLabelText | pelo texto da label | getByLabelText(labelText, selector) |

## Jest matchers

O Jest disponibiliza alguns `matchers` para validação dos testes, vou passar alguns abaixo com base nos exemplos de testes que vão ser realizados:

| Matcher | Validação |
| :-----: | :-------: |
| toBe(value) | valor passado |
| toHaveLength(number) | tamanho de array ou string passado |
| toHaveBeenCalledTimes(number) | número de chamadas realizado |

## testing-library/jest-dom

Fornece `matchers` adicionais em relação aos que já estão presentes pelo Jest. 

``import "@testing-library/jest-dom"``

Segue alguns exemplos abaixo:

| Matcher | Validação |
| :-----: | :-------: |
| toBeInTheDocument() | presença de elemento |
| toBeDisabled() | elemento desabilitado |
| toHaveTextContent(text) | conteúdo de texto |

## Exemplos iniciais de testes

Nesse primeiro exemplo, vamos ter um componente de botão que através da const `clickedNumber` registra o número de cliques nele, pela função `onClick`. Se o número de cliques é maior que zero, ele mostra a quantidade de cliques na tela. Além disso ele aceita receber uma props `disabled`, que uma vez que é passada desabilita o botão:

```js
import React, { useState } from "react";

const BaseComponent = ({ disabled }) => {
  const [clickedNumber, setClickedNumber] = useState(0);

  const onClick = () => {
    setClickedNumber(clickedNumber + 1);
  };

  return (
    <>
      <button 
        data-testid="baseButton" 
        onClick={onClick} 
        disabled={disabled}
      >
        Acionamentos
      </button>
      {clickedNumber > 0 
        && <p data-testid="baseParagraph">{clickedNumber}</p>}
    </>
  );
};

```

No bloco de teste abaixo vão ser validadas duas coisas:

- Teste 1: se após renderizar o componente o botão está presente
- Teste 2: se o parágrafo que traz a quantidade de cliques não está presente (pois o botão não foi clicado)

```js
import React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";

import BaseComponent from "./BaseComponent";

describe("<BaseComponent />", () => {
  beforeEach(() => {
    render(<BaseComponent />);
  });

  it("should bring button element", () => {
    const element = screen.getByRole("button", { name: "Acionamentos" });
    // const element = screen.getByText("Acionamentos");
    // const element = screen.getByTestId("baseButton");

    expect(element).toBeInTheDocument();
  });

  it("should not bring paragraph element", () => {
    const element = screen.queryByTestId("baseParagraph");

    expect(element).not.toBeInTheDocument();
  });
});

```

Como em ambos os testes o componente vai ser renderizado da mesma forma foi colocado um `beforeEach` antes deles com a renderização.
No primeiro teste coloquei três formas de buscar o botão: pela role dele (onde é passado a busca pela role `button` e em name o texto dele `Acionamentos`), pelo texto dele diretamente e pelo testId dele (corresponde ao data-testid presente no componente). Por fim é validado se o botão está presente a partir do matcher `toBeInTheDocument()`.
O segundo teste verifica a não presença do parágrafo, dado que não foi clicado o botão. Nesse caso ao invés de fazer a busca pelo `getBy` foi feito a busca pelo `queryBy`, pois o `getBy` sem encontrar o elemento quebraria o teste, mas o intuito do teste é verificar a não presença do elemento mesmo. Usando `queryBy` o teste não quebra (pois traz `null` como resposta da busca) e é possível verificar a não presença pela negação do matcher `toBeInTheDocument()`.

Após a execução dos testes, eles passam com sucesso:

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

De cima para baixo é possível observar:

- arquivo que foi executado: `src/BaseComponent.test.js`
- bloco que foi executado: `<BaseComponent />`
- testes que foram executados: ambos os testes com a descrição colocada para eles e um positivo a esquerda mostrando que passaram
- Test Suites: a quantidade de blocos de testes executados e quantos passaram 
- Tests: a quantidade de testes executados e quantos passaram

Para exemplificar o resultados de falha foi modificada a busca do parágrafo do segundo teste para usar o `getBy`:

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

Além das informações passadas acima, no caso de falha é mostrado:

- Teste que falhou com um x a esquerda dele
- Informações gerais do teste que falhou: em vermelho a descrição do bloco e do teste que falhou, abaixo como estava o componente renderizado no momento da falha e mais para baixo a linha do teste que falhou
- Test Suites: do total de um bloco diz que um falhou, pois um bloco de teste falha desde que qualquer teste dentro dele falhe
- Tests: de dois testes, um passou e um falhou

Agora, para testar se o botão ficará desabilitado ou não, serão feitos dois testes renderizando o componente de duas formas diferentes, passando ou não a props `disabled`:

- Teste 1: renderizado o `BaseComponent` sem passar a props `disabled`
- Teste 2: renderizado o `BaseComponent` passando a props `disabled`

```js
import React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";

import BaseComponent from "./BaseComponent";

describe("<BaseComponent />", () => {
  it("should bring button element enabled", () => {
    render(<BaseComponent />);

    const element = screen.getByRole("button", { name: "Acionamentos" });

    expect(element).not.toBeDisabled();
  });

  it("should bring button element disabled with disabled props", () => {
    render(<BaseComponent disabled />);

    const element = screen.getByRole("button", { name: "Acionamentos" });

    expect(element).toBeDisabled();
  });
});

```

Em ambos os teste foi usado o matcher `toBeDisabled()`, o primeiro para validar a não desativação do botão uma vez que não foi passada a props `disabled`, negando o matcher na validação. O segundo foi para validar a desativação por passar a props `disabled` para o componente.

## testing-library/user-event

Lib que vai possibilitar a simulação da interação que o usuário terá com o componente.

``import userEvent from "@testing-library/user-event"``

| User event | Ação |
| :--------: | :--: |
| click(), dblClick() | clique, duplo clique |
| selectOptions() | seleção de uma opção |
| paste() | cola de texto |
| type() | escrita de texto |
| upload() | upload de arquivo |

## Exemplo de teste de interação

Nesse exemplo será usado o mesmo componente dos exemplos iniciais acima, a diferença é que vão ocorrer outras validações:

- Teste 1: validar o aparecimento do parágrafo com a quantidade de cliques após um clique do botão, validando se com o valor 1
- Teste 2: validar o aparecimento do parágrafo com a quantidade de cliques após um clique duplo do botão, validando se com o valor 2

```js
import React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import BaseComponent from "./BaseComponent";

describe("<BaseComponent />", () => {
  beforeEach(() => {
    render(<BaseComponent />);
  });

  it("should bring paragraph with clicked quantity after button click", () => {
    const buttonElement = screen.getByRole("button", { name: "Acionamentos" });

    userEvent.click(buttonElement);

    const paragraphElement = screen.queryByTestId("baseParagraph");

    expect(paragraphElement).toBeInTheDocument();
    expect(paragraphElement).toHaveTextContent(1);
  });

  it("should bring paragraph with clicked quantity after double button click", () => {
    const buttonElement = screen.getByRole("button", { name: "Acionamentos" });

    userEvent.dblClick(buttonElement);

    const paragraphElement = screen.queryByTestId("baseParagraph");

    expect(paragraphElement).toBeInTheDocument();
    expect(paragraphElement).toHaveTextContent(2);
  });
});

```

No primeiro teste é simulado um clique no botão a partir do user event `click`, validando após esse clique o aparecimento do parágrafo a partir do matcher `toBeInTheDocument()`, e a quantidade que aparece a partir do matcher `toHaveTextContent()`. No segundo teste são feitas as validações com os mesmos matchers, só variando a quantidade dentro do `toHaveTextContent()`, e para simular o click duplo é usado o user event `dblClick`.

## Mock de funções

Permite fazer o mock de funções presentes dentro do componente, setState(), requisições.

``const mockFunction = jest.fn()``

| Análise | Código |
| :-----: | :----: |
| Chamadas da função | mockFunction.mock.calls |
| Variáveis passadas nas chamadas | mockFunction.mock.calls[0][0] |
| Resultado da chamada | mockFunction.mock.results[0].value |
| Limpar todos os mocks | jest.clearAllMocks() |

- jest.clearAllMocks(): é usado para limpar a função de mock entre os testes, pois a limpeza das chamadas da função não ocorre de forma automática (vai acumulando ao decorrer dos testes se não fizer a limpeza)
- mockFunction.mock.calls: o primeiro [ ] corresponde a qual chamada da função está sendo analisada, o segundo [ ] corresponde a qual variável dessa chamada que está sendo analisada

Por exemplo, tendo uma função f(x, y) que durante os testes foi chamada duas vezes:

- mockFunction.mock.calls[0][0]: valor de x da primeira chamada
- mockFunction.mock.calls[0][1]: valor de y da primeira chamada
- mockFunction.mock.calls[1][0]: valor de x da segunda chamada
- mockFunction.mock.calls[1][1]: valor de y da segunda chamada

## Exemplo de teste de mock

Para realizar os testes de mock, será utilizado um novo componente, que é um campo input com label `Valor:`, que recebe um setState via props `setValue`. Toda vez que é modificado o texto dentro do input aciona uma função `handleChange`, que chama o `setValue` passando o valor atual presente dentro do campo input:

```js
import React from "react";

const BaseComponent = ({ setValue }) => {
  const handleChange = (e) => {
    setValue(e.target.value);
  };

  return (
    <label>
      Valor:
      <input type="text" onChange={(e) => handleChange(e)} />
    </label>
  );
};

export default BaseComponent;

```

Serão realizados dois testes, mockando o `setValue` para validar quantas vezes ele é chamado e o valor que é passado dentro dele:

- Teste 1: será digitado o texto 10 no campo input usando o userEvent `type`, que corresponde a duas mudanças do campo input (pois para digitar 10, primeiro se digita o 1 e depois se digita o 0)
- Teste 2: será colado o texto 10 usando o userEvent `paste`, que corresponde a uma mudança do campo input

```js
import React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import BaseComponent from "./BaseComponent";

const setValue = jest.fn();

describe("<BaseComponent />", () => {
  beforeEach(() => {
    render(<BaseComponent setValue={setValue} />);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it("should call setValue after type on input", () => {
    const element = screen.getByLabelText("Valor:", { selector: "input" });

    userEvent.type(element, "10");

    expect(setValue).toHaveBeenCalledTimes(2);
    // expect(setValue.mock.calls).toHaveLength(2);
    expect(setValue.mock.calls[0][0]).toBe("1");
    expect(setValue.mock.calls[1][0]).toBe("10");
  });

  it("should call setValue after paste on input", () => {
    const element = screen.getByLabelText("Valor:", { selector: "input" });

    userEvent.paste(element, "10");

    expect(setValue).toHaveBeenCalledTimes(1);
    expect(setValue.mock.calls[0][0]).toBe("10");
  });
});

```

No começo do teste é mockado o `setValue` a partir do `jest.fn()`. Como em ambos os testes é renderizado o componente da mesma forma, a renderização é colocada dentro de uma `beforeEach`. Devido a função de mock acumular as chamadas realizadas a ela, não limpando entre os testes automaticamente, as chamadas são limpadas após a execução de cada teste devido ao `afterEach` realizando `jest.clearAllMocks()`.
A busca do campo input é realizada a partir da label dele `Valor:` e passando o tipo de seletor `input`.
No primeiro teste que é referente a digitação foi usado o user event `type` passando o valor 10, mostrando duas formas de validar a quantidade de chamadas ao `setValue`, vendo diretamente quantas vezes a função de mock foi chamada pelo matcher `toHaveBeenCalledTimes()`, sendo possível ver também pelo tamanho do array que registra as chamadas (setValue.mock.calls) pelo matcher `toHaveLength()`. Por fim é verificado com que valor foi chamado o `setValue`, usando o `setValue.mock.calls[0][0]` para a primeira chamada e o `setValue.mock.calls[1][0]` para a segunda.
O segundo teste faz as mesmas validações, mas a diferença é que ao invés da digitação de 10 esse número é colado diretamente usando o user event `paste`.

## Conclusão

A ideia foi apresentar de forma geral como funciona os testes unitários usando Jest com testing library, abordando estrutura dos testes, renderização de componente, busca de elemento dentro do componente renderizado, simulação de interação do usuário com o componente, mock de funções e validações nos testes.
Mas o uso dessas libs possibilitam vários outros tipos de testes a serem realizados, por essa razão estou colocando os principais links separados por temas para quem quiser se aprofundar mais.

## Links

[Estrutura testes](https://jestjs.io/pt-BR/docs/api)
[Queries](https://testing-library.com/docs/queries/about/)
[Roles](https://www.w3.org/TR/html-aria/#docconformance)
[User events](https://testing-library.com/docs/user-event/intro/)
[Matchers Jest](https://jestjs.io/docs/expect)
[Matchers testing-library](https://github.com/testing-library/jest-dom)
[Funções de simulação](https://jestjs.io/pt-BR/docs/mock-functions)