Ok, que testar suas aplicações antes de enviá-las para produção é importante todo mundo já sabe. Mas será que você sabe como testar eficientemente sua aplicação escrita em Node.js?
Neste tutorial veremos como utilizar o módulo Jest para criar testes de unidade (unit tests) em uma abordagem de desenvolvimento orientado à testes, o TDD (Test Driven Development). O mais legal é que não nos restringiremos apenas a testar funções isoladas (unit test), também vamos testar APIs completas em Node.js usando o módulo Supertest para forjar requisições HTTP (integration test).
Veremos neste post:
Vamos lá!
Introdução ao TDD
O Test Driven Development/TDD ou Desenvolvimento Orientado à Testes é uma técnica criada por Kent Beck, um famoso programador e autor de livros que atualmente trabalha no Facebook. Também são de autoria de Beck a metodologia ágil Extreme Programming (XP) que inclui técnicas como o TDD e o Pair Programming, que já falei aqui no blog.
A ideia do TDD é que você deve escrever primeiro os testes da sua aplicação, para depois implementar o código que fará com que eles funcionem. Isso pode soar um tanto estranho mas é uma ideia ousada que possui vários benefícios, tais como:
- diminuição do número de bugs, uma vez que não existem features sem testes;
- foco nas features que importam para o projeto, pois escrevemos os testes com os requisitos em mãos;
- permite testes de regressão, para ver se um sistema continua funcionando mesmo após várias mudanças e/ou muito tempo desde a última release;
- aumento da confiança do time no código programado;
O TDD é executado em um ciclo chamado de Red/Green/Refactor e prega que cada incremento pequeno de software (baby step) deve ser testado, para que bugs sejam corrigidos rapidamente assim que surgem. Ele prega que primeiro você deve escrever o teste, antes mesmo da feature ser implementada. Quando executar esse teste, ele irá falhar (Red), aí você codifica a feature apenas o suficiente para ela passar no teste (Green) e então você melhora o código para que ele não apenas seja eficaz, mas eficiente (Refactor).
Recomenda-se rodar novamente os testes e se após a refatoração a feature parar de funcionar, roda-se o ciclo novamente.
O coração do TDD são os unit tests.
Introdução aos Testes de Unidade
Existe uma pequena discussão sobre a melhor tradução de “unit test” ser “teste unitário” ou “teste de unidade”. Acredito que seja uma discussão mais conceitual do que prática (como muitas discussões “técnicas” que existem por aí), então não entrarei nela aqui e apenas chamarei de “teste de unidade”.
Um unit test é um teste, geralmente automatizado, que testa uma única unidade da sua aplicação, geralmente uma única função, em um único contexto.
Cheio de “únicos” nesse parágrafo, não?
Mesmo com essa ênfase ainda existem muitas dúvidas e discussões acerca do que pode ser considerado um unit test ou não. Me aterei mais à prática e menos aos conceitos e resumirei como: se o seu teste testa apenas uma coisa (como uma função), chamarei ele de teste de unidade aqui, mesmo essa coisa internamente seja composta de outras coisas (como outras funções internas) afinal, parto do pressuposto que não temos como garantir que algumas funções do Node.js também não chamam diversas outras internamente.
Os unit testes são o coração do TDD pois é com eles que começamos a aplicar TDD. Mesmo que você não seja um “purista” e vá aplicar TDD 100% como manda os livros, tente ao menos passar a utilizar testes de unidade na sua aplicação, pois eles realmente valem a pena.
Um exemplo prático de aplicação de testes de unidade com TDD seria a criação de um método que aplica um desconto, em R$, ao valor de um produto, também em R$. Vamos começar escrevendo a função de teste dessa funcionalidade em um arquivo index.js, comparando o retorno da função aplicarDesconto com o valor esperado, o que chamamos de asserção ou assert (em Inglês):
1 2 3 4 5 6 7 8 |
function aplicarDescontoTest(){ return aplicarDesconto(10,2) === 8; } console.log('A aplicação de desconto está funcionando? '); console.log(aplicarDescontoTest()); |
Se aplicarmos um desconto de R$2 sobre um produto de R$10, o valor esperado como retorno é R$8, certo? Mas o que acontece se executarmos este bloco de código com o comando “node index” no terminal?
Dará um erro porque a função aplicarDesconto ainda não existe. Ou seja, sabemos o resultado esperado, mas não programamos a função ainda. Vamos fazê-lo agora, com uma simples subtração, no mesmo arquivo:
1 2 3 4 5 |
function aplicarDesconto(valor, desconto){ return valor - desconto; } |
Agora se rodarmos o arquivo index.js no terminal novamente ele deve indicar que está funcionando, pois ao testar nossa função passando 10 e 2, ela retornará 8 e a asserção será verdadeira. Sendo assim, nosso trabalho com esta função terminou, certo?
Errado. E se o valor do desconto for superior ao produto? Você conhece alguma loja que te paga para comprar alguma coisa? Eu não!
Quando uma unidade de código (nossa função nesse caso) possui casos de uso variados, devemos testá-los sob cada contexto, em asserções separadas. Sendo assim, vamos criar um novo teste considerando que, um produto jamais possa ter um valor negativo, mesmo com descontos altos (ficando de graça nesse caso):
1 2 3 4 5 6 7 |
function aplicarDescontoGrandeTest(){ return aplicarDesconto(1,10) === 0; } console.log('A aplicação de desconto grande está funcionando? '); console.log(aplicarDescontoGrandeTest()); |
Note que essa é uma regra que inventei para esse teste. Se o desconto for maior que o valor, o produto deve sair de graça. Se mandar rodar esse arquivo novamente, verá que esse segundo teste não irá ‘passar’, embora o teste inicial continue passando.
Vamos refatorar nossa função aplicarDesconto para que contemple este cenário que acabamos de descobrir:
1 2 3 4 5 6 |
function aplicarDesconto(valor, desconto){ if(desconto > valor) return 0; return valor - desconto; } |
Rode novamente seu arquivo de testes e verá que agora os dois testes ‘passam’!
Criar todos os testes necessários para garantir que todas as unidade do seu software realmente funcionam é o que chamamos de cobertura de código (code coverage), cuja utópica marca de 100% deve ser sempre o ideal, embora praticamente inalcançável em sistemas complexos.
E se o desconto for negativo? Precisamos de um teste pra isso, pois um desconto não pode aumentar o valor original de um produto!
E se o valor do produto for negativo? Isso é claramente um erro de integridade dos erros, pois é economicamente impossível!
O quão ‘detalhado’ os seus testes serão vai muito do grau de importância que todas essas questões possuem para o seu negócio e o quanto domina ele.
Algumas vezes, a sua nova refatoração para fazer com que a função atenda a um novo requisito (como não permitir descontos negativos, por exemplo) pode fazer com que algum teste antigo deixe de passar. E isso é bom. Sempre que asserções deixam de funcionar durante os testes é hora de reanalisar o seu algoritmo e melhorá-lo. Antes descobrir essa falha durante os testes do que em produção, não é mesmo!
Agora veremos como criar testes de unidade mais profissionais usando o módulo Jest.
Criando Unit Tests com Jest
Você até pode escrever testes de unidade sem módulo algum, apenas usando Node.js puro como fiz acima, sendo que o mínimo que deveria fazer é separar as funções de verdade das funções de teste, em módulos.
No entanto, facilita muito a vida usar bibliotecas como Jest, Tape, Mocha e Chai. Para este tutorial vamos usar o Jest, que é muito popular atualmente, permite fazer asserções de uma maneira muito simples, prática e padronizada, dando agilidade ao processo de unit testing ou TDD, caso leve a metodologia realmente a sério. Além disso ele cria informações de cobertura de testes muito legais.
Vamos começar criando uma pasta para um novo projeto, chamado tdd-jest. Crie um index.js e um index.test.js nessa pasta. O primeiro será o arquivo da nossa aplicação e o segundo os unit tests do primeiro.
Abra o terminal e navegue até a pasta em questão. Execute o seguinte comando:
1 2 3 |
npm init |
Esse comando serve para criação de um package.json para o seu projeto. Apertando Enter em cada uma das perguntas que ele lhe fará no console usa a configuração padrão. Atente apenas ao fato de que quando chegar a pergunta ‘test command’, digite o seguinte comando:
1 2 3 |
jest |
Apenas confirme as demais perguntas pressionando Enter e quando terminar, abra o arquivo package.json e verá algo parecido com isso:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "name": "tdd-jest", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "jest" }, "author": "LuizTools", "license": "ISC" } |
Note que tem uma seção scripts ali. Nesta seção, definimos atalhos para comandos padrões que poderemos quer executar via NPM, como o comando “test” que é executado no console com:
1 2 3 |
npm test |
Mas não vamos fazer isso agora, afinal, não criamos nossos testes com Jest ainda. Então instale agora a dependência do Jest digitando o seguinte comando (ela será instalada como dependência de desenvolvimento):
1 2 3 |
npm install --save-dev jest |
Agora vamos inicializar as configurações do Jest em nosso projeto digitando o comando abaixo na pasta do mesmo.
1 2 3 |
jest --init |
O terminal irá lhe fazer algumas perguntas.
- Sobre TypeScript, escolha não.
- Sobre ambiente de teste, escolha Node.
- Sobre coverage reports, escolha yes.
- Sobre provider de instrument code, use v8.
- E sobre limpeza automática de mock, escolha yes.
Um arquivo jest.config.js será criado na raiz do seu projeto com as configurações que o Jest usará para rodar os testes que escrevermos.
E depois abra o arquivo index.test.js para configurarmos nossos testes. Escreva o primeiro teste de unidade como abaixo:
1 2 3 4 5 6 7 8 |
//index.test.js const index = require('./index') test('Aplicar desconto', () => { const result = index.aplicarDesconto(10,5); expect(result).toEqual(5); }) |
A função test espera o nome do teste e uma função de teste que realizará a asserção (expect) e junto dela usamos uma série de funções como toEqual, toNotEqual, toBeTruthy, toBeFalsy e muito mais. Essa é uma asserção bem simples e fácil de entender, não?!
Se você rodar agora com “npm test”:
1 2 3 |
TypeError: index.aplicarDesconto is not a function |
Isso porque essa função ainda não existe. Vamos criá-la tal qual criamos no exemplo anterior sobre testes de unidade, mas no arquivo index.js, exportando-a via module.exports:
1 2 3 4 5 6 7 8 9 |
//index.js function aplicarDesconto(valor, desconto){ if(desconto > valor) return 0; return valor - desconto; } module.exports = { aplicarDesconto } |
Agora teste novamente com ‘npm test’ e verá uma saída bem interessante.
Essa saída do Jest mostra os testes que foram executados, o resultado de cada um e um total de testes e testes bem sucedidos, finalizando com uma mensagem de ok ou não.
Podemos incrementar ainda mais os resultados dos testes, mas antes, vamos adicionar mais um teste de unidade, tentando aplicar um desconto superior ao valor do produto em nosso index.test.js (já fizemos isso antes, lembra?):
1 2 3 4 5 6 |
test('Aplicar desconto grande', () => { const result = index.aplicarDesconto(5,10); expect(result).toEqual(0); }) |
E para incrementar o resultado dos testes, vamos adicionar estatísticas de code coverage neles (cobertura de testes de código), indo no arquivo jest.config.js e habilitando a opção collectCoverage com true.
1 2 3 |
collectCoverage: true, |
Agora, executando o projeto com ‘npm test’, terá como resultado:
Juntando este conhecimento com ferramentas de Continuous Integration (CI) como CircleCI e Jenkins, podemos incluir testes de unidade automatizados em nosso build para evitar que um código suba para produção com algum bug (um dia falarei disso por aqui).
Mas e como podemos fazer testes mais amplos, como testes em APIs escritas em Node.js? Afinal, é preciso subir um servidor Express para testar uma API, certo? Caso contrário não teríamos objetos de request e response válidos…
Mas testes de integração é assunto para este outro tutorial!
Agora se quer aprender sobre mocking, técnica importantíssima para unit tests, leia este artigo!
E se quiser aprender a fazer testes unitários de front-end, leia este tutorial!
Espero que tenham gostado do tutorial e que apliquem em seus projetos!
Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu livro sobre programação web com Node.js!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.