Testes unitários em React com Jest e testing library

griseduardo

Eduardo Henrique Gris

Posted on April 29, 2024

Testes unitários em React com Jest e testing library

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…", () => {
      …
  })
})


Enter fullscreen mode Exit fullscreen mode

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



Enter fullscreen mode Exit fullscreen mode

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



Enter fullscreen mode Exit fullscreen mode

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

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

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


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



Enter fullscreen mode Exit fullscreen mode

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



Enter fullscreen mode Exit fullscreen mode

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;



Enter fullscreen mode Exit fullscreen mode

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



Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
griseduardo
Eduardo Henrique Gris

Posted on April 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related