Continuando a série sobre React Native, neste artigo iremos cobrir como criar testes automatizados para nossa aplicação. Caso não esteja acompanhando, abaixo estão os links para os artigos anteriores:

Lá no primeiro artigo, iniciamos a nossa aplicação com o create-react-native-app, e com ele já temos o Jest configurado para rodar nossos testes automatizados, inclusive o CRNA, já criou um teste para nós, o App.test.js.  Mas antes de partimos para ação, vamos fazer uma breve contextualização sobre como iremos testar a nossa app.

Por que e como testar apps em React Native?

A razão de testar, e no nosso caso, de forma automatizada, são muitas, as principais são:

  • Garantir o correto funcionamento da aplicação de forma automatizada
  • Nos permitir fazer refatorações, novas funcionalidades e mudanças, com um rápido feedback se quebramos algo ou não
  • Ganhar tempo, evitando testes manuais que são mais demorados
  • Exercitar cenários de erro, com maior facilidade e rapidez, afinal, é muito mais simples simular um retorno inválido de uma API, durante o teste, do que testando manualmente

No cenário do React Native, iremos utilizar o Jest, um test runner com esteróides, por já vir todo configurado com o coverage, mocks, assertions e testes de snapshot. Ou seja, é uma verdadeira bazuca, e para aplicações React e React Native faz sentido utilizar-la, pois iremos precisar dessas funcionalidades já providas por padrão.

Criando nosso primeiro teste

Para rodar os testes, basta executar yarn test ou npm test, que irá rodar o comando presente no “scripts”=>”test” do nosso package.json. Como já temos um teste criado, você verá que o mesmo está quebrado (embora ele passe), com o erro: ReferenceError: XMLHttpRequest is not defined

Esse erro é mais do que esperado, pois os testes rodam com o Node.js, e não no browser, e o XMLHttpRequest, só existe por padrão no browser. E assim começa a nossa jornada no mundo dos testes automatizados.

Testar além de todos os beneficios que comentamos, é uma oportunidade de desacoplar o nosso código da infraestrutura, pois no contexto do teste, muitos detalhes precisam ser abstraídos, quando estamos fazendo testes de integração e unitários, afinal eles implicam em estarmos isolando o nosso código, para que eles não virem um teste de sistema. Aliás, se para você esses níveis de testes são confusos, segue uma breve descrição deles:

  • unitário: testes validando apenas um comportamento, isolado este comportamento de dependências que podem existir. Geralmente é um teste de apenas uma classe/método
  • integração: testes validando apenas um comportamento, mas com interação das suas dependências. Parecido com o teste unitário, mas sem o isolamento
  • sistema: testes da perspectiva do usuário (seja um usuário da API, uma aplicação, ou do usuário de uma app), utilizando os mesmos mecanimos de input e output que o usuário utiliza, sem isolamentos de dependências, ou seja, frente ao sistema rodando (o termo sistema, é o mais genérico de todos, que muitas vezes é chamado de test end-to-end, teste de aceitação e broad-stack test – usado no artigo do Martin Fowler)

Voltando ao erro da falta do XMLHttpRequest, temos algumas opções para resolver:

  1. Instalar o node-fetch ou o jest-fetch-mock, como dependência de dev
  2. Fazer o mock manualmente do XMLHttpRequest, no global
  3. Trocarmos o fetch pelo axios, que também funciona com Node.js, e não representa uma grande mudança na nossa implementação

Qual abordagem você escolheria? Acredito que a maioria iria para a 1, afinal estamos acostumados a instalar libs, e a mágia acontecer (isso é bom, mas também perigoso). A instalação de uma nova dependência, para substituir outra usada em produção, adiciona o risco de a implementação real, no caso o fetch mudar, e os nossos testes não quebrarem, afinal não usam ele. Claro que isso é um cenário raro, pois o node-fetch por exemplo, está de acordo com a especificação do fetch, mas não deixa de ser um risco.

Já a segunda opção é insana (embora divertida), pois você terá que conhecer as implementações do fetch e reimplementar mockando elas.

Agora a terceira opção, confesso que também acho meio “overkill”, trocar uma dependência, “só” para os testes? Sim, é isso que iremos fazer 🙂 – para evitar os riscos citados acima e seguir o “mandamento” não “mockaras” o código do próximo.

No fim, ainda iremos nos beneficiar de uma decisão que fizemos anteriormente, de isolar o uso do fetch em um service que faz o wrapper dele. Com isso a mudança de código é mínima, e mostrada a seguir (não esqueça de instalar o axios, com yarn add axios):

Agora ao rodar novamente os testes com yarn test, o teste irá passar novamente, mas dessa vez sem erros no console. E provavelmente você deve está se perguntando: como que o teste passava, sendo que estava com um erro? O problema está na fragilidade do assert que o CRNA gerou para nós, que como um gerador em si não é tão problemático, o problema é seguirmos o mesmo padrão nos nossos testes.

O teste gerado foi o seguinte:

const rendered = renderer.create(<App />).toJSON();
expect(rendered).toBeTruthy();

Na primeira linha, está sendo usado o react-test-render, para renderizar nossa app sem o ambiente nativo, retornando a app num objeto JavaScript, e fazemos ainda o parse para JSON dele. Já na segunda linha, é que está o grande problema, a verificação/assertion não é determinística, afinal testar usando toBeTruthy, irá retornar true sempre que tivermos algum valor no rendered (pode ser tanto true, uma string, objeto, número, etc), ainda mais que estamos testando a renderização do App em si, e não a existência de algum componente dentro dele.

Nosso primeiro teste então, será melhorar esse teste, que no final atende o que ele testa “renders without crashing”,  porém queremos ser mais preciso na validação.


No teste acima, temos algumas melhorias em cima do anterior:

  • O nome do teste descreve um cenário de uso, que é o momento que o usuário acessa a app, e as seleções ainda estão sendo carregadas
  • Estamos sendo mais determinísticos, pois embora ainda usamos o toBeTruthy, estamos fazendo a assertion em cima do componente principal para o teste, que é neste caso o ActivityIndicatorRendered, que se renderizado, é sinal que o loading está sendo apresentado

Porém, este teste ainda iria passar, no cenário anterior, que estávamos usando o fetch, isso pois o fetch faz parte um processo assíncrono, e o nosso teste é síncrono e não depende do fetch ocorrer com sucesso, pois estamos testando um momento antes.

Caso o nosso componente de loading não fosse apenas uma gif, poderíamos fazer a assert, baseada no seu conteúdo, ao invés, de apenas na existência do componente em si. Porém esse não é a abordagem correta, pois estaríamos trazendo um detalhe do TeamsList, para o teste do App. Portanto sempre tenha em mente, que os testes não podem ser em cima de efeitos de outro código, além do que estamos testando.

Chegou a hora de criarmos mais um teste: um teste assíncrono

Testando código assíncrono

Primeiro vamos criar uma nova pasta, chamada test e dentro dela 3 novas pastas para cada nível de teste:

  • unit
  • integration
  • e2e

Vamos mover o teste que criamos para a pasta e2e: mv App.test.js test/e2e/

Além disso, é preciso altera os seguintes imports:

import TeamsList from '../../src/components/TeamsList';
import App from '../../App';

Se você já viu outros projetos em React/React Native, pode estar estrando a separação do teste da implementação, por geralmente os próprios geradores criarem o teste junto a implementação, como é o caso do próprio App.test.js.

Estamos fazendo uso por dois motivos principais:

  • Ter uma visão clara do nível do teste
  • Não necessariamente precisamos criar um teste para cada implementação, e isso inclusive faria acoplarmos demais o teste a implementação, por isso separando, conseguimos fugir dessa “armadilha”. Uma vez, que muitos arquivos podem ser testados via testes de integração

Agora crie o arquivo test/integration/services/httpRequest.test.js

Ainda estamos utilizando o sufixo .test, pois caso no futuro desejarmos organizar os testes de outra forma (por exemplo, junto a implementação), a mudança seja menor.

Sobre o teste é importante notar alguns pontos:

  • ele é um teste de integração, cujo objetivo é testar a integração com o axios, por isso não mockamos ele (e também devido ao mandamento que falei anteriormente)
  • embora seja de integração, não podemos depender de dependências externas, no caso a nossa API estar online. Por isso o uso do nock, para fazer o papel da nossa API (instale ele com yarn add –dev nock, ele é um dev dependency, pois só precisamos para os testes)
  • dados utilizados nos testes, devem ser abstraídos como fixtures, pois além de melhorar a leitura do teste, também podem ser reutilizados
  • como estamos testando um código assíncrono, precisamos usa o async na nossa função de teste
  • adicionamos os comentários given, when e then mais com o intuito de facilitar a leitura do teste, pode parecer descenessário pois estamos com o código e teste fresco na cabeça, mas daqui 6 meses, ou para uma pessoa nova no projeto, irá ajudar
  • ao criar testes é importante pensar tanto no cenário feliz, como nos cenários alternativos/exceção
  • utilizamos o describe para contextualizar o teste, e ele ajudar na visualização dos resultados ao rodar o yarn test
  • e por fim, nosso teste é agnóstico do React/React Native

Durante o teste, notei um bug no tratamento de erro no src/services/httpRequest.js, que estava retornando o objeto direto de erro do axios, e não um parse dele. O código abaixo corrige o bug:

Com o teste pronto e a correção feita, basta rodar yarn test para rodar os testes.

Assim finalizamos mais um artigo da série, no próximo iremos continuar testando nossa aplicação, pois se você observar (rodando yarn test --coverage --collectCoverageFrom=src/**/*.{js,jsx} e abrindo o arquivo gerado em coverage/lcov-report/index.html), faltam alguns testes para garantir o correto funcionamento da aplicação, e por assim uma boa cobertura de testes.

Até a próxima!

O código apresentado está disponível no Github