Recentemente escrevi um artigo ensinando sobre como usar a técnica de mocking, muito popular e útil para TDD, com a suíte de testes Jest, para aplicações Node.js. No entanto, senti que alguns conceitos importantes não foram abordados e que podem levar a um uso equivocado da técnica, impactando principalmente na cobertura de código da sua aplicação (code coverage).
Se você usa Jest há algum tempo, deve saber que um dos grandes benefícios dele em relação a outras suítes como Tape é a coleta de coverage metrics. Ele não apenas roda suas baterias de testes automatizados como faz a análise e apresenta métricas da proporção de testes varrendo todo o seu código, como na imagem acima.
No entanto, quando começamos a mockar nossas funções, a tendência é que os testes comecem a percorrer as funções fake e não as reais, portanto caindo drasticamente a nossa cobertura de código pois ele de fato não está sendo testado, mas sim mockado. Por exemplo, o relatório abaixo é de um projeto real, que estava com +80% de cobertura de código com testes mas que caiu drasticamente por causa de apenas dois pequenos módulos que foram mockados.
Mas o que fazer nestes casos então, devemos usar mocks e abandonar coverage? Ou devemos focar em coverage e abandonar mocks?
Nenhum dos dois, devemos primeiro entender o que devemos e mockar e o que não devemos mockar!
O que devemos mockar?
O intuito e benefícios de utilizar mocking foi dito no último artigo, mas talvez não tenha ficado claro, então vou explicar de forma diferente.
Partes da sua aplicação como um todo não são de sua gestão ou controle. Por exemplo, sua aplicação usa alguma API externa que você chama com o Axios ou pacote semelhante? Se essa API se comporta de maneira estranha, você mexe no fonte dela, escreve testes e commita no repo? Até poderia, se ela for uma API sua ou de um projeto open-source, mas esse é um exemplo de aspecto da sua aplicação que você não gerencia, mas depende para o seu código funcionar, toda vez que precisa fazer um request para ela.
O mesmo vale para o seu banco de dados e outros módulos instalados via NPM. Sua aplicação depende deles para funcionar, mas você não gerencia o código deles, apenas usa, geralmente através de pacotes que, apesar de serem open-source muitas vezes, você não gerencia com frequência também.
Esses pacotes e recursos externos não-gerenciáveis por você, cujo código você usa mas não mexe, são os fortes candidatos a serem mockados. Porque eles costumam muitas vezes tornar os seus testes dependentes, acoplados, lentos e falhos, além de exigir muito setup e cleanup, antes e depois dos testes respectivamente.
Tentando trazer de uma maneira bem prática sob o viés de APIs externas, se você se focar em mockar os requests para elas ao invés de seus módulos clientes que as chamam, você estará atacando o mal da dependência na raiz e ao mesmo tempo poderá seguir buscando uma alta cobertura de testes no SEU código, pois o código da node_modules e chamadas HTTP através do Axios não entram no coverage report.
Para ajudar a tornar esta explicação mais convincente, vou trazer um case que acho que pode ajudar: mocks de URL.
Mock de URL
É muito comum as nossas aplicações não serem ilhas e fazerem diversas integrações com outros serviços através de web APIs REST ou SOAP. Por exemplo, uma integração muito comum é com a API do IBGE para estados e municípios. Se você acessar https://servicodados.ibge.gov.br/api/v1/localidades/estados no seu navegador você vai entender o que estou falando.
Assim, podemos escrever um código JS bem simples com Axios que chama esta API para retornar todas UFs brasileiras rapidamente. Vamos chamar este módulo de ibgeService:
1 2 3 4 5 6 7 8 |
//ibgeService module.exports.getUFs = async () => { const axios = require('axios'); const result = await axios.get('https://servicodados.ibge.gov.br/api/v1/localidades/estados'); return result.data; } |
Para testá-lo rapidamente, podemos escrever um index.js que usa este módulo e imprime o resultado.
1 2 3 4 5 6 7 |
(async () => { const ibgeService = require('./ibgeService'); const ufs = await ibgeService.getUFs(); console.log(ufs) })(); |
Assim, quando executamos o projeto acima, temos o seguinte resultado.
E se quisermos escrever testes para este nosso módulo, podemos fazê-lo com o Jest, criando um ibgeService.test.js dentro de uma pasta __tests__
1 2 3 4 5 6 7 8 9 |
//__tests__/ibgeService.test.js const ibgeService = require('../ibgeService'); test('getUFs', async () => { const result = await ibgeService.getUFs(); expect(Array.isArray(result)).toBeTruthy(); }) |
O que acontece se eu rodar este teste? Vai dar tudo certo, provavelmente.
No entanto, diferente desta API que é aberta e que não costuma gerar problemas, existem uma série de APIs que exigem infraestrutura específica para seus testes, exigem autenticação complexa ou mesmo podem gerar problemas se forem chamadas muitas vezes durante testes, como a AWS que cobra por uso de algumas APIs ou apenas deixar tudo muito lento.
Para resolver isso, podemos mockar a API-alvo usando uma dentre as diversas opções disponíveis:
- módulo de mock específico da API: a AWS por exemplo possui um pacote específico de mocking;
- módulo de mock do cliente HTTP: existem alguns módulos específicos que mockam clientes HTTP como o Moxios que serve para mockar Axios.
- mock de URL: aqui a gente não mocka um módulo específico, mas chamadas HTTP para uma URL específica;
Vamos usar este último recurso que é muito poderoso e prático ao mesmo tempo.
Como mockar uma URL?
Se você pegar o que ensinei no último tutorial e não pensar muito a respeito, a primeira coisa que você faria seria criar uma pasta __mocks__ e colocar um ibgeService.js dentro dela, mockando a função getUFs, certo?
No entanto, se você fizer isso, você não estará testando a API do IBGE (ok) e nem o seu ibgeService (não ok), sua cobertura de teste ficará baixíssima. E isso não é legal. O SEU código DEVE estar sendo testado e com uma boa cobertura de testes ainda por cima (>80%). Sendo assim, você NÃO deve mockar ele, mas sim, mockar o código ou chamadas DOS OUTROS, como a chamada HTTP à API do IBGE, por exemplo.
Para isso, você pode usar o pacote nock que facilita bastante esta atividade. Com ele, você aponta a URL que o seu cliente HTTP vai chamar e ele vai interceptar as chamadas a nível de biblioteca HTTP do Node.js e substituir pelo seu retorno mockado.
Assim, primeiro instale a dependência do nock na sua aplicação, como desenvolvimento.
1 2 3 |
npm i -D nock |
Depois, volte ao seu ibgeService.test.js e antes do teste, configure o nock para interceptar a chamada à API do IBGE.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//__tests__/ibgeService.test.js const ibgeService = require('../ibgeService'); const nock = require('nock'); nock('https://servicodados.ibge.gov.br') .persist() .get('/api/v1/localidades/estados') .reply(200, [{ id: 53, sigla: 'DF', nome: 'Distrito Federal', regiao: { id: 5, sigla: 'CO', nome: 'Centro-Oeste' } }]) test('getUFs', async () => { const result = await ibgeService.getUFs(); expect(Array.isArray(result)).toBeTruthy(); }) |
O uso do Nock é bem similar ao de bibliotecas cliente de HTTP. Você configura uma URL base nele e chama a função com o nome do método HTTP e o path a serem interceptados. O resultado (função reply) é o status code e o body da resposta HTTP (mockados).
A diferença aqui é a função persist, que chamei ali pois por padrão o nock só intercepta uma chamada HTTP e depois ele deixa as demais passarem. Com persist, ele intercepta todas.
Assim, em qualquer ponto do código seguinte que houver uma request para a rota de UFs do IBGE, ela irá disparar o nock retornando o objeto que defini. Esse teste irá funciona independente se a API está up ou down, se exige ou não autenticação, se tem dados disponíveis para lhe responder, etc. Ele é um teste unitário da função getUFs do ibgeService. Ponto.
Rodando os testes agora, você manterá uma cobertura de teste alta sobre o SEU código. O código da API do IBGE, esse sim não estará sendo testado, ou sequer a infraestrutura deles, mas isso não é responsabilidade dos testes unitários, certo?
Você ainda pode querer testes integrados, mas em menor volume, bem como ainda pode querer testes manuais, em volume menor ainda, mas o seu código, esse sim você deve ter um grande volume e rodar muitas e muitas vezes a cada alteração no projeto, adição de funcionalidades, refatorações, antes de deploy, etc.
E para conseguir isso sem ter uma série de problemas, você precisa aplicar técnicas como mocking corretamente. Por isso escrevi este artigo. 🙂
Espero ter ajudado.
Um abraço e sucesso.
Quer ver na prática como utilizar escrever testes de um software real, usando a stack completa do JavaScript? Conheça meu curso clicando no banner abaixo!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.