Como Testar um Aplicativo React com Jest e React Testing Library

O autor selecionou Vets Who Code para receber uma doação como parte do programa Write for DOnations.

Introdução

Obter uma cobertura de teste sólida é imperativo para construir confiança em sua aplicação web. Jest é um executor de testes JavaScript que fornece recursos para escrever e executar testes. React Testing Library oferece um conjunto de ajudantes de teste que estruturam seus testes com base nas interações do usuário, em vez de detalhes de implementação de componentes. Tanto Jest quanto React Testing Library vêm pré-embalados com Create React App e seguem o princípio orientador de que testar aplicativos deve se assemelhar a como o software será usado.

Neste tutorial, você testará código assíncrono e interações em um projeto de exemplo contendo vários elementos de interface do usuário. Você usará o Jest para escrever e executar testes unitários, e implementará o React Testing Library como uma biblioteca de DOM (Document Object Model) auxiliar para lidar com a interação com componentes.

Pré-requisitos

Para completar este tutorial, você precisará de:

  • Node.js versão 14 ou superior instalado em sua máquina local. Para instalar o Node.js no macOS ou Ubuntu 18.04, siga as etapas em Como Instalar o Node.js e Criar um Ambiente de Desenvolvimento Local no macOS ou a seção Instalando Usando um PPA de Como Instalar o Node.js no Ubuntu 18.04.

  • npm versão 5.2 ou superior em sua máquina local, que você precisará usar Create React App e npx no projeto de exemplo. Se você não instalou npm junto com Node.js, faça isso agora. Para Linux, use o comando sudo apt install npm.

    • Para que os pacotes do npm funcionem neste tutorial, instale o pacote build-essential. Para Linux, use o comando sudo apt install build-essential.
  • Git instalado em sua máquina local. Você pode verificar se o Git está instalado em seu computador ou passar pelo processo de instalação para seu sistema operacional com Como Instalar o Git no Ubuntu 20.04.

  • Familiaridade com o React, que você pode desenvolver com a série Como Programar em React.js. Como o projeto de exemplo é inicializado com Create React App, você não precisa instalá-lo separadamente.

  • Alguma familiaridade com o Jest como um executor ou framework de testes é útil, mas não obrigatória. Como o Jest é pré-empacotado com o Create React App, você não precisa instalá-lo separadamente.

Passo 1 — Configurando o Projeto

Neste passo, você irá clonar um projeto de exemplo e iniciar a suíte de testes. O projeto de exemplo utiliza três ferramentas principais: Create React App, Jest e React Testing Library. Create React App é usado para inicializar uma aplicação React de página única. Jest é usado como o executor de testes, e React Testing Library fornece ajudantes de teste para estruturar testes em torno de interações do usuário.

Para começar, você irá clonar um aplicativo React pré-construído do GitHub. Você trabalhará com o aplicativo Doggy Directory, que é um projeto de exemplo que utiliza a Dog API para construir um sistema de busca e exibição para uma coleção de imagens de cachorro baseadas em uma raça específica.

Para clonar o projeto do Github, abra seu terminal e execute o seguinte comando:

  1. git clone https://github.com/do-community/doggy-directory

Você verá uma saída semelhante a esta:

Output
Cloning into 'doggy-directory'... remote: Enumerating objects: 64, done. remote: Counting objects: 100% (64/64), done. remote: Compressing objects: 100% (48/48), done. remote: Total 64 (delta 21), reused 55 (delta 15), pack-reused 0 Unpacking objects: 100% (64/64), 228.16 KiB | 3.51 MiB/s, done.

Mude para a pasta doggy-directory:

  1. cd doggy-directory

Instale as dependências do projeto:

  1. npm install

O comando npm install irá instalar todas as dependências do projeto definidas no arquivo package.json.

Após instalar as dependências, você pode visualizar a versão implantada do aplicativo ou você pode executar o aplicativo localmente com o seguinte comando:

  1. npm start

Se você optar por executar o aplicativo localmente, ele será aberto em http://localhost:3000/. Você verá a seguinte saída no terminal:

Output
Compiled successfully! You can now view doggy-directory in the browser. Local: http://localhost:3000 On Your Network: http://network_address:3000

Depois de iniciar, a página inicial do aplicativo terá esta aparência:

O projeto foi instalado as dependências e o aplicativo está agora em execução. Em seguida, abra um novo terminal e execute os testes com o seguinte comando:

  1. npm test

O comando npm test inicia os testes em um modo interativo de observação com o Jest como seu executor de testes. Quando no modo de observação, os testes são executados automaticamente após a alteração de um arquivo. Os testes serão executados sempre que você alterar um arquivo e informarão se essa alteração passou nos testes.

Após executar o npm test pela primeira vez, você verá esta saída no terminal:

Output
No tests found related to files changed since last commit. Press `a` to run all tests, or run Jest with `--watchAll`. Watch Usage › Press a to run all tests. › Press f to run only failed tests. › Press q to quit watch mode. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press Enter to trigger a test run.

Agora que você tem o exemplo de aplicativo e conjunto de testes em execução, você pode começar a testar com a página inicial.

Passo 2 — Testando a Página Inicial

Por padrão, o Jest procurará por arquivos com o sufixo .test.js e arquivos com o sufixo .js nas pastas __tests__. Quando você fizer alterações nos arquivos de teste relevantes, elas serão detectadas automaticamente. À medida que os casos de teste são modificados, a saída será atualizada automaticamente. O arquivo de teste preparado para o projeto de exemplo doggy-directory está configurado com código mínimo antes de você adicionar paradigmas de teste. Neste passo, você escreverá testes para verificar se a página inicial do aplicativo será carregada antes de realizar uma pesquisa.

Abra src/App.test.js no seu editor para ver o seguinte código:

src/App.test.js
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders the landing page', () => {
  render(<App />);
});

A minimum of one test block is required in each test file. Each test block accepts two required parameters: the first argument is a string representing the name of the test case; the second argument is a function that holds the expectations of the test.

Dentro da função, há um método render que o React Testing Library fornece para renderizar seu componente no DOM. Com o componente que você deseja testar renderizado no DOM do ambiente de teste, agora você pode começar a escrever código para assert contra a funcionalidade esperada.

Você adicionará um bloco de teste ao método render que testará se a página de destino é renderizada com precisão antes de quaisquer chamadas de API ou seleções serem feitas. Adicione o código destacado abaixo do método render:

src/App.test.js
...
test('renders the landing page', () => {
  render(<App />);
  
  expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/);
  expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed");
  expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
  expect(screen.getByRole("img")).toBeInTheDocument();
});

A função expect é usada toda vez que você deseja verificar um determinado resultado, e ela aceita um único argumento representando o valor que seu código produz. A maioria das funções expect é associada a uma função matcher para afirmar algo sobre um valor específico. Para a maioria dessas asserções, você usará matchers adicionais fornecidos por jest-dom para tornar mais fácil verificar aspectos comuns encontrados no DOM. Por exemplo, .toHaveTextContent é o matcher para a função expect na primeira linha, enquanto getByRole("heading") é o seletor para pegar o elemento DOM.

O React Testing Library fornece o objeto screen como uma maneira conveniente de acessar as consultas pertinentes necessárias para afirmar contra o ambiente DOM de teste. Por padrão, o React Testing Library fornece consultas que permitem localizar elementos dentro do DOM. Existem três categorias principais de consultas:

  • getBy* (mais comumente usado)
  • queryBy* (usado ao testar a ausência de um elemento sem lançar um erro)
  • findBy* (usado ao testar código assíncrono)

Cada tipo de consulta serve a um propósito específico que será definido posteriormente no tutorial. Neste passo, você se concentrará na consulta getBy*, que é o tipo de consulta mais comum. Para ver uma lista exaustiva das diferentes variações de consulta, você pode revisar o guia de consulta do React.

Abaixo está uma imagem anotada da página inicial do Diretório de Cães indicando cada seção que o primeiro teste (na renderização da página inicial) abrange:

Cada função expect está fazendo uma asserção contra o seguinte (mostrado na imagem anotada acima):

  1. Você espera que o elemento com o papel de cabeçalho tenha uma correspondência de substring de Diretório de Cães.
  2. Você espera que a entrada select tenha um valor de exibição exato de Selecione uma raça.
  3. Você espera que o botão Buscar esteja desativado, pois uma seleção não foi feita.
  4. Você espera que a imagem de espaço reservado esteja presente no documento, já que uma busca ainda não foi realizada.

Quando terminar, salve o arquivo src/App.test.js. Como os testes estão sendo executados no modo de observação, as alterações serão registradas automaticamente. Se as alterações não forem registradas automaticamente, talvez seja necessário parar e reiniciar o conjunto de testes.

Agora, ao visualizar seus testes no terminal, você verá a seguinte saída:

Output
PASS src/App.test.js ✓ renders the landing page (172 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 2.595 s, estimated 5 s Ran all test suites related to changed files. Watch Usage: Press w to show more.

Neste passo, você escreveu um teste inicial para verificar a visualização inicial da página de destino do Diretório de Cachorros. No próximo passo, você aprenderá como simular uma chamada de API para testar código assíncrono.

Passo 3 — Simulando o Método fetch

Neste passo, você revisará uma abordagem para simular o método fetch do JavaScript. Embora existam várias maneiras de fazer isso, esta implementação usará os métodos spyOn e mockImplementation do Jest.

Quando você depende de APIs externas, há uma chance de que a API deles fique fora do ar ou leve um tempo para retornar uma resposta. Simular o método fetch fornece um ambiente consistente e previsível, dando mais confiança aos seus testes. Um mecanismo de simulação de API é necessário para executar corretamente testes que usam uma API externa.

Nota: Em um esforço para manter este projeto simplificado, você irá simular o método fetch. No entanto, é aconselhável usar uma solução mais robusta como o Mock Service Worker (MSW) ao simular código assíncrono para bases de código maiores e prontas para produção.

Abra o arquivo src/mocks/mockFetch.js no seu editor para revisar como o método mockFetch funciona:

src/mocks/mockFetch.js
const breedsListResponse = {
    message: {
        boxer: [],
        cattledog: [],
        dalmatian: [],
        husky: [],
    },
};

const dogImagesResponse = {
    message: [
        "https://images.dog.ceo/breeds/cattledog-australian/IMG_1042.jpg ",
        "https://images.dog.ceo/breeds/cattledog-australian/IMG_5177.jpg",
    ],
};

export default async function mockFetch(url) {
    switch (url) {
        case "https://dog.ceo/api/breeds/list/all": {
            return {
                ok: true,
                status: 200,
                json: async () => breedsListResponse,
            };
        }
        case "https://dog.ceo/api/breed/husky/images" :
        case "https://dog.ceo/api/breed/cattledog/images": {
            return {
                ok: true,
                status: 200,
                json: async () => dogImagesResponse,
            };
        }
        default: {
            throw new Error(`Unhandled request: ${url}`);
        }
    }
}

O método mockFetch retorna um objeto que se assemelha de perto à estrutura do que uma chamada de fetch retornaria em resposta a chamadas de API dentro da aplicação. O método mockFetch é necessário para testar a funcionalidade assíncrona em duas áreas do aplicativo Doggy Directory: o dropdown de seleção que preenche a lista de raças e a chamada de API para recuperar imagens de cachorro quando uma busca é realizada.

Feche o arquivo src/mocks/mockFetch.js. Agora que você entende como o método mockFetch será usado em seus testes, você pode importá-lo para o seu arquivo de teste. A função mockFetch será passada como argumento para o método mockImplementation e será usada como uma implementação falsa da API fetch.

No arquivo src/App.test.js, adicione as linhas destacadas de código para importar o método mockFetch:

src/App.test.js
import { render, screen } from '@testing-library/react';
import mockFetch from "./mocks/mockFetch";
import App from './App';

beforeEach(() => {
   jest.spyOn(window, "fetch").mockImplementation(mockFetch);
})

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

Este código irá configurar e desmontar a implementação simulada para que cada teste comece em uma base nivelada.

jest.spyOn(window, "fetch"); cria uma função simulada que irá rastrear chamadas ao método fetch anexado à variável global window no DOM.

.mockImplementation(mockFetch); aceita uma função que será usada para implementar o método de simulação. Como este comando substitui a implementação original de fetch, ele será executado sempre que fetch for chamado dentro do código do aplicativo.

Ao terminar, salve o arquivo src/App.test.js.

Agora, ao visualizar seus testes no terminal, você receberá a seguinte saída:

Output
console.error Warning: An update to App inside a test was not wrapped in act(...). When testing, code that causes React state updates should be wrapped into act(...): act(() => { /* fire events that update state */ }); /* assert on the output */ This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act at App (/home/sammy/doggy-directory/src/App.js:5:31) 18 | }) 19 | .then((json) => { > 20 | setBreeds(Object.keys(json.message)); | ^ 21 | }); 22 | }, []); 23 | ... PASS src/App.test.js ✓ renders the landing page (429 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.178 s, estimated 2 s Ran all test suites related to changed files.

O aviso informa que uma atualização de estado ocorreu quando não era esperado. No entanto, a saída também indica que os testes simularam com sucesso o método fetch.

Neste passo, você simulou o método fetch e incorporou esse método a um conjunto de testes. Embora o teste esteja passando, ainda é necessário resolver o aviso.

Passo 4 — Corrigindo o Aviso act

Neste passo, você aprenderá como corrigir o aviso act que surgiu após as alterações no Passo 3.

O aviso act ocorre porque você simulou o método fetch e, quando o componente é montado, ele faz uma chamada de API para obter a lista de raças. A lista de raças é armazenada em uma variável de estado que popula o elemento option dentro do input select.

A imagem abaixo mostra como o input select fica após uma chamada de API bem-sucedida para popular a lista de raças:

O aviso é lançado porque o estado é definido após o bloco de teste terminar de renderizar o componente.

Para corrigir esse problema, adicione as modificações destacadas ao caso de teste em src/App.test.js:

src/App.test.js
...
test('renders the landing page', async () => {
   render(<App />);
   
   expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/);
   expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed");
   expect(await screen.findByRole("option", { name: "husky"})).toBeInTheDocument();
   expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
   expect(screen.getByRole("img")).toBeInTheDocument();
});

A palavra-chave async informa ao Jest que o código assíncrono é executado como resultado da chamada de API que ocorre quando o componente é montado.

A new assertion with the findBy query verifies that the document contains an option with the value of husky. findBy queries are used when you need to test asynchronous code that is dependent on something being in the DOM after a period of time. Because the findBy query returns a promise that gets resolved when the requested element is found in the DOM, the await keyword is used within the expect method.

Ao terminar, salve as alterações feitas em src/App.test.js.

Com as novas adições, você verá que o aviso act não está mais presente nos seus testes:

Output
PASS src/App.test.js ✓ renders the landing page (123 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.942 s, estimated 2 s Ran all test suites related to changed files. Watch Usage: Press w to show more.

Neste passo, você aprendeu como corrigir o aviso act que pode ocorrer ao trabalhar com código assíncrono. Em seguida, você adicionará um segundo caso de teste para verificar as funcionalidades interativas da aplicação Doggy Directory.

Passo 5 — Testando a Funcionalidade de Busca

No último passo, você escreverá um novo caso de teste para verificar a funcionalidade de busca e exibição de imagens. Você utilizará uma variedade de consultas e métodos de API para alcançar a cobertura de teste adequada.

Retorne ao arquivo src/App.test.js no seu editor. No topo do arquivo, importe a biblioteca complementar user-event e o método assíncrono waitForElementToBeRemoved para o arquivo de teste com os comandos destacados:

src/App.test.js
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';import userEvent from '@testing-library/user-event'; 
...

Você usará esses imports mais tarde nesta seção.

Após o método test() inicial, adicione um novo bloco de teste assíncrono e renderize o componente App com o seguinte bloco de código:

src/App.test.js
...
test("should be able to search and display dog image results", async () => {
   render(<App />);
})

Com o componente renderizado, agora você pode adicionar funções que verificam os recursos interativos do aplicativo Doggy Directory.

Ainda no arquivo src/App.test.js, adicione os blocos de código destacados dentro do segundo método test():

src/App.test.js
...
test("should be able to search and display dog image results", async () => {
   render(<App />);
   
   //Simular a seleção de uma opção e verificar seu valor
   const select = screen.getByRole("combobox");
   expect(await screen.findByRole("option", { name: "cattledog"})).toBeInTheDocument();
   userEvent.selectOptions(select, "cattledog");
   expect(select).toHaveValue("cattledog");
})

A seção destacada acima irá simular a seleção de uma raça de cachorro e verificar se o valor correto é exibido.

A consulta getByRole pega o elemento selecionado e o atribui à variável select.

Assim como você corrigiu o aviso de act no Passo 4, use a consulta findByRole para esperar que a opção cattledog apareça no documento antes de prosseguir com mais assertivas.

O objeto userEvent importado anteriormente irá simular interações comuns do usuário. Neste exemplo, o método selectOptions seleciona a opção cattledog pela qual você esperou na linha anterior.

A última linha assegura que a variável select contenha o valor cattledog selecionado acima.

A próxima seção que você irá adicionar ao bloco Javascript test() irá iniciar a solicitação de busca para encontrar imagens de cachorros com base na raça selecionada e confirmar a presença de um estado de carregamento.

Adicione as linhas destacadas:

src/App.test.js
...
test("should be able to search and display dog image results", async () => {
   render(<App />);
    
   //...Simular a seleção de uma opção e verificar seu valor

  //Simular a inicialização da solicitação de busca
   const searchBtn = screen.getByRole("button", { name: "Search" });
   expect(searchBtn).not.toBeDisabled();
   userEvent.click(searchBtn);

   //O estado de carregamento é exibido e removido assim que os resultados são exibidos
   await waitForElementToBeRemoved(() => screen.queryByText(/Loading/i));
})

A consulta getByRole localiza o botão de busca e o atribui à variável searchBtn.

O matcher toBeDisabled do jest-dom verificará se o botão de busca não está desativado quando uma seleção de raça é feita.

O método click no objeto userEvent simula clicar no botão de busca.

A função waitForElementToBeRemoved assíncrona auxiliar importada anteriormente irá esperar pelo aparecimento e desaparecimento da mensagem Loading enquanto a chamada da API de pesquisa está em andamento. queryByText dentro do retorno de chamada de waitForElementToBeRemoved verifica a ausência de um elemento sem lançar um erro.

A imagem abaixo mostra o estado de carregamento que será exibido quando uma pesquisa estiver em andamento:

Em seguida, adicione o seguinte código Javascript para validar a imagem e a exibição do número de resultados:

src/App.test.js
...
test("should be able to search and display dog image results", async () => {
   render(<App />)
   
   //...Simular a seleção de uma opção e verificar seu valor
   //...Simular o início da solicitação de pesquisa
   //...O estado de carregamento é exibido e removido uma vez que os resultados são exibidos
          
   //Verificar exibição de imagem e contagem de resultados
   const dogImages = screen.getAllByRole("img");
   expect(dogImages).toHaveLength(2);
   expect(screen.getByText(/2 Results/i)).toBeInTheDocument();
   expect(dogImages[0]).toHaveAccessibleName("cattledog 1 of 2");
   expect(dogImages[1]).toHaveAccessibleName("cattledog 2 of 2");
})

A consulta getAllByRole irá selecionar todas as imagens de cães e atribuí-las à variável dogImages. A variante *AllBy* da consulta retorna uma matriz contendo vários elementos que correspondem ao papel especificado. A variante *AllBy* difere da variante ByRole, que só pode retornar um único elemento.

A implementação simulada de fetch continha dois URLs de imagem dentro da resposta. Com o comparador toHaveLength do Jest, você pode verificar se há duas imagens exibidas.

A consulta getByText verificará se a contagem de resultados apropriada aparece no canto direito.

Duas asserções usando os matchers toHaveAccessibleName verificam se o texto alternativo apropriado está associado às imagens individuais.

A completed search displaying images of the dog based on the breed selected along with the number of results found will look like this:

Quando você combina todas as partes do novo código JavaScript, o arquivo App.test.js ficará assim:

src/App.test.js
import {render, screen, waitForElementToBeRemoved} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import mockFetch from "./mocks/mockFetch";
import App from './App';

beforeEach(() => {
   jest.spyOn(window, "fetch").mockImplementation(mockFetch);
})

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

test('renders the landing page', async () => {
   render(<App />);

   expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/);
   expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed");
   expect(await screen.findByRole("option", { name: "husky"})).toBeInTheDocument()
   expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
   expect(screen.getByRole("img")).toBeInTheDocument();
});

test("should be able to search and display dog image results", async () => {
   render(<App />);

   // Simula a seleção de uma opção e verifica o seu valor
   const select = screen.getByRole("combobox");
   expect(await screen.findByRole("option", { name: "cattledog"})).toBeInTheDocument();
   userEvent.selectOptions(select, "cattledog");
   expect(select).toHaveValue("cattledog");

   // Inicia a solicitação de pesquisa
   const searchBtn = screen.getByRole("button", { name: "Search" });
   expect(searchBtn).not.toBeDisabled();
   userEvent.click(searchBtn);

   // O estado de carregamento é exibido e removido assim que os resultados são exibidos
   await waitForElementToBeRemoved(() => screen.queryByText(/Loading/i));

   // Verifica a exibição da imagem e a contagem de resultados
   const dogImages = screen.getAllByRole("img");
   expect(dogImages).toHaveLength(2);
   expect(screen.getByText(/2 Results/i)).toBeInTheDocument();
   expect(dogImages[0]).toHaveAccessibleName("cattledog 1 of 2");
   expect(dogImages[1]).toHaveAccessibleName("cattledog 2 of 2");
})

Salve as alterações feitas em src/App.test.js.

Ao revisar seus testes, a saída final no terminal agora terá a seguinte saída:

Output
PASS src/App.test.js ✓ renders the landing page (273 ms) ✓ should be able to search and display dog image results (123 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 4.916 s Ran all test suites related to changed files. Watch Usage: Press w to show more.

Nesta etapa final, você adicionou um teste que verifica as funcionalidades de pesquisa, carregamento e exibição do aplicativo Doggy Directory. Com a asserção final escrita, agora você sabe que seu aplicativo funciona.

Conclusão

Ao longo deste tutorial, você escreveu casos de teste usando Jest, React Testing Library e os matchers jest-dom. Construindo incrementalmente, você escreveu testes baseados em como um usuário interage com a interface do usuário. Você também aprendeu as diferenças entre as consultas getBy*, findBy* e queryBy* e como testar código assíncrono.

Para saber mais sobre os tópicos mencionados acima, dê uma olhada na documentação oficial do Jest, React Testing Library e jest-dom. Você também pode ler o Common Mistakes with React Testing Library de Kent C. Dodd para aprender as melhores práticas ao trabalhar com React Testing Library. Para mais informações sobre como usar testes de snapshot em um aplicativo React, confira How To Write Snapshot Tests.

Source:
https://www.digitalocean.com/community/tutorials/how-to-test-a-react-app-with-jest-and-react-testing-library