Aumente a capacidade de manutenção do código com o teste de integração do React
Publicados: 2022-03-11Os testes de integração são um ponto ideal entre custo e valor dos testes. Escrever testes de integração para um aplicativo React com a ajuda de react-testing-library em vez de ou além de testes de unidade de componentes pode aumentar a capacidade de manutenção do código sem prejudicar a velocidade de desenvolvimento.
Caso você queira obter uma vantagem antes de prosseguirmos, você pode ver um exemplo de como usar react-testing-library para testes de integração de aplicativos React aqui.
Por que investir em testes de integração?
"Os testes de integração estabelecem um grande equilíbrio nas trocas entre confiança e velocidade/despesa. É por isso que é aconselhável gastar a maior parte (não todo, lembre-se) de seu esforço lá."
– Kent C. Dodds em testes de escrita. Não muito. Principalmente integração.
É uma prática comum escrever testes de unidade para componentes React, geralmente usando uma biblioteca popular para testar “enzimas” React; especificamente, seu método “raso”. Essa abordagem nos permite testar componentes isoladamente do restante do aplicativo. No entanto, como escrever aplicativos React tem tudo a ver com composição de componentes, os testes de unidade por si só não garantem que o aplicativo esteja livre de bugs.
Por exemplo, alterar as props aceitas de um componente e atualizar seus testes de unidade associados pode resultar na aprovação de todos os testes enquanto o aplicativo ainda pode ser interrompido se outro componente não tiver sido atualizado adequadamente.
Os testes de integração podem ajudar a preservar a tranquilidade ao fazer alterações em um aplicativo React, pois garantem que a composição dos componentes resulte no UX desejado.
Requisitos para testes de integração de aplicativos React
Aqui estão algumas das coisas que os desenvolvedores do React querem fazer ao escrever testes de integração:
- Teste os casos de uso do aplicativo da perspectiva do usuário. Os usuários acessam informações em uma página da Web e interagem com os controles disponíveis.
- Chamadas de API simuladas para não depender da disponibilidade e estado da API para testes de aprovação/reprovação.
- APIs de navegador simuladas (por exemplo, armazenamento local), pois elas simplesmente não existem no ambiente de teste.
- Afirme no estado React DOM (DOM do navegador ou um ambiente móvel nativo).
Agora, para algumas coisas que devemos tentar evitar ao escrever testes de integração de aplicativos React:
- Detalhes da implementação do teste. As mudanças de implementação só devem interromper um teste se de fato introduzirem um bug.
- Zomba demais. Queremos testar como todas as partes do aplicativo estão funcionando juntas.
- Renderização rasa. Queremos testar a composição de todos os componentes do aplicativo até o menor componente.
Por que escolher a biblioteca de testes do React?
Os requisitos mencionados acima fazem da react-testing-library uma ótima escolha, pois seu principal princípio orientador é permitir que os componentes do React sejam testados de uma maneira que se assemelhe a como eles são usados por um humano real.
A biblioteca, juntamente com suas bibliotecas complementares opcionais, nos permite escrever testes que interagem com o DOM e afirmam seu estado.
Exemplo de configuração de aplicativo
O aplicativo para o qual vamos escrever testes de integração de amostra implementa um cenário simples:
- O usuário insere um nome de usuário do GitHub.
- O aplicativo exibe uma lista de repositórios públicos associados ao nome de usuário inserido.
Como a funcionalidade acima é implementada deve ser irrelevante do ponto de vista do teste de integração. No entanto, para se manter próximo dos aplicativos do mundo real, o aplicativo segue padrões comuns do React, daí o aplicativo:
- É um aplicativo de página única (SPA).
- Faz solicitações de API.
- Tem gerenciamento de estado global.
- Apoia a internacionalização.
- Utiliza uma biblioteca de componentes React.
O código-fonte para a implementação do aplicativo pode ser encontrado aqui.
Escrevendo testes de integração
Instalando dependências
Com fio:
yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock
Ou com npm:
npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock
Criando um arquivo do conjunto de testes de integração
Vamos criar um arquivo chamado viewGitHubRepositoriesByUsername.spec.js
na pasta ./test
do nosso aplicativo. Jest irá buscá-lo automaticamente.
Importando dependências no arquivo de teste
import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test
Configurando o conjunto de testes
describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });
Notas:
- Antes de todos os testes, zombe da API do GitHub para retornar uma lista de repositórios quando chamada com um nome de usuário específico.
- Após cada teste, limpe o teste React DOM para que cada teste comece a partir de um ponto limpo.
-
describe
blocos especificam o caso de uso de teste de integração e as variações de fluxo. - As variações de fluxo que estamos testando são:
- O usuário insere um nome de usuário válido que tem repositórios GitHub públicos associados.
- O usuário insere um nome de usuário válido que não tem repositórios GitHub públicos associados.
- O usuário insere um nome de usuário que não existe no GitHub.
-
it
bloqueia o uso de retorno de chamada assíncrono, pois o caso de uso que eles estão testando tem uma etapa assíncrona.
Escrevendo o primeiro teste de fluxo
Primeiro, o aplicativo precisa ser renderizado.
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
O método render
importado do módulo @testing-library/react
renderiza o aplicativo no teste React DOM e retorna consultas DOM vinculadas ao contêiner do aplicativo renderizado. Essas consultas são usadas para localizar elementos DOM para interagir e fazer declarações.
Agora, como a primeira etapa do fluxo em teste, o usuário recebe um campo de nome de usuário e digita uma string de nome de usuário nele.
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
O auxiliar userEvent
do módulo importado @testing-library/user-event
tem um método de type
que imita o comportamento do usuário quando ele digita texto em um campo de texto. Ele aceita dois parâmetros: o elemento DOM que aceita a entrada e a string que o usuário digita.

Os usuários geralmente encontram elementos DOM pelo texto associado a eles. No caso de entrada, é um texto de rótulo ou um texto de espaço reservado. O método de consulta getByPlaceholderText
retornado anteriormente da render
nos permite encontrar o elemento DOM pelo texto do espaço reservado.
Observe que, como o próprio texto geralmente muda, é melhor não confiar nos valores de localização reais e, em vez disso, configurar o módulo de localização para retornar uma chave de item de localização como seu valor.
Por exemplo, quando a localização “en-US” normalmente retornaria Enter GitHub username
como o valor para a chave userSelection.usernamePlaceholder
, em testes, queremos que ela retorne userSelection.usernamePlaceholder
.
Quando o usuário digita texto em um campo, ele deve ver o valor do campo de texto atualizado.
expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
Em seguida no fluxo, o usuário clica no botão enviar e espera ver a lista de repositórios.
userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');
O método userEvent.click
imita o usuário clicando em um elemento DOM, enquanto a consulta getByText
encontra um elemento DOM pelo texto que ele contém. O modificador closest
garante que selecionemos o elemento do tipo certo.
Observação: em testes de integração, as etapas geralmente atendem a funções de act
e assert
. Por exemplo, afirmamos que o usuário pode clicar em um botão clicando nele.
Na etapa anterior, afirmamos que o usuário vê a seção de lista de repositórios do aplicativo. Agora, precisamos afirmar que, como a busca da lista de repositórios do GitHub pode levar algum tempo, o usuário vê uma indicação de que a busca está em andamento. Também queremos garantir que o aplicativo não informe ao usuário que não há repositórios associados ao nome de usuário inserido enquanto a lista de repositórios ainda está sendo buscada.
getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();
Observe que o prefixo de consulta getBy
é usado para afirmar que o elemento DOM pode ser encontrado e o prefixo de consulta queryBy
é útil para a afirmação oposta. Além disso, queryBy
não retorna um erro se nenhum elemento for encontrado.
Em seguida, queremos garantir que, eventualmente, o aplicativo termine de buscar os repositórios e os exiba para o usuário.
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));
O método assíncrono waitForElement
é usado para aguardar uma atualização do DOM que tornará a asserção fornecida como parâmetro do método true. Nesse caso, afirmamos que o aplicativo exibe o nome e a descrição de cada repositório retornado pela API GitHub simulada.
Por fim, o aplicativo não deve mais exibir um indicador de que os repositórios estão sendo buscados e não deve exibir uma mensagem de erro.
expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();
Nosso teste de integração React resultante se parece com isso:
it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });
Testes de fluxo alternativo
Quando o usuário insere um nome de usuário do GitHub sem repositórios públicos associados, o aplicativo exibe uma mensagem apropriada.
describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });
Quando o usuário insere um nome de usuário do GitHub que não existe, o aplicativo exibe uma mensagem de erro.
describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });
Por que os testes de integração React Rock
O teste de integração realmente oferece um ponto ideal para aplicativos React. Esses testes ajudam a detectar bugs e usam a abordagem TDD e, ao mesmo tempo, não requerem manutenção quando a implementação muda.
A biblioteca de testes do React, apresentada neste artigo, é uma ótima ferramenta para escrever testes de integração do React, pois permite que você interaja com o aplicativo como o usuário faz e valide o estado e o comportamento do aplicativo da perspectiva do usuário.
Esperamos que os exemplos fornecidos aqui ajudem você a começar a escrever testes de integração em projetos React novos e existentes. O código de exemplo completo que inclui a implementação do aplicativo pode ser encontrado no meu GitHub.