Testes unitários em React com Jest e testing library
Eduardo Henrique Gris
Posted on April 29, 2024
Introdução
No artigo 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:
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)
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:
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
:
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 propsdisabled
- Teste 2: renderizado o
BaseComponent
passando a propsdisabled
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
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:
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
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
Queries
Roles
User events
Matchers Jest
Matchers testing-library
Funções de simulação
Posted on April 29, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.