Pirâmide de testes: uma busca por qualidade

October 03, 2022
Escrito por

Ultimamente tenho visto muitas perguntas sobre como os testes funcionam na programação, então decidi fazer um post explicando um pouco do meu ponto de vista e das minhas pesquisas sobre o que são testes.

Vale ressaltar que quando falamos de testes, costumamos falar sobre uma "Pirâmide de testes", conceito introduzido por Mike Cohn no livro Succeeding with Agile (2009).

Você pode achar muitas imagens referentes a ela, mas em quase todos os casos você encontrará essa representação:

piramide de testes

Vamos nos aprofundar nesses tópicos, mas antes gostaria de começar o post com uma pequena introdução sobre o que são testes por definição. Nada melhor como encontrar seu significado no dicionário e estes foram os mais chamaram minha atenção:

  • qualquer meio para verificar ou testar a qualidade ou a veracidade de algo; prova, exame, verificação.
  • exame crítico ou prova das qualidades de uma pessoa ou coisa.

Agora que sabemos o que é teste, vamos levar isso para o mundo do código. Se o significado do teste é verificar a qualidade ou a veracidade de algo, podemos assumir que nós vamos testar e garantir a qualidade do nosso código escrito, certo?

Temos ciência do que está sendo testado, vamos entender quais são os possíveis testes que podemos aplicar em nossos códigos.

Testes unitários

De acordo com a Wikipedia, na programação de computadores, o `teste de unidade` é um método de teste de software pelo qual unidades individuais de código-fonte.

Vamos digerir um pouco essa informação: se nós estamos testando unidades individuais do código, nós não queremos testar qualquer outra coisa que está fora do escopo do assunto, o que significa que em certos momentos, nós teremos que simular e forçar respostas de partes do código.

Ficou complexo? Vamos entender um pouco isso escrevendo um pouco de código:

Vamos dizer que nossa função tem a responsabilidade de pegar a cotação atual de uma criptomoeda utilizando API REST e adicionar 1% a mais no valor dessa cotação!

const getCotacaoAPI = require("./getCotacaoAPI.js");
 
function cotacaoComMultiplicador() {
   const cotacao = getCotacaoAPI.getCotacaoAPI(); 
   const cotacaoComMultiplicador = cotacao * 1.01;
   return cotacaoComMultiplicador;
}
 
console.log(cotacaoComMultiplicador())
 
module.exports = cotacaoComMultiplicador;

Essa função parece estar funcionando bem! Vamos roda-lá e ver o resultado.

teste_resultado

Agora nós sabemos que o resultado da função é 510.05! Vamos então escrever um teste unitário para ela.

const { expect } = require('chai');
 
const cotacaoComMultiplicador = require('./cotacaoComMultiplicador.js');
 
describe('cotacaoComMultiplicador', () => {
   it('cotacaoComMultiplicador deve retornar 510.05', () => {
       const resposta = cotacaoComMultiplicador();
       expect(resposta).equal(510.05);
   });
});

E para roda-lá, eu vou usar a biblioteca Mocha que é uma biblioteca de testes em Javascript!

resultado_falho

Nossos testes falharam! Mas por quê? Certo, vamos lembrar que a função `getCotacaoAPI` pode retornar qualquer valor, considerando que a cotação da nossa criptomoeda é muito volátil!

Repare que o resultado esperado era 510.05 mas nós recebemos 357.54. Agora, como podemos resolver isso? A resposta é Stubs!

Na programação, stubs são objetos simulados que imitam o comportamento de objetos reais de maneira controlada, na maioria das vezes como parte de uma iniciativa de teste de software.

Praticamente todas as bibliotecas de testes possuem um módulo de stubs robusto, pois é completamente necessário para o nosso dia a dia de desenvolvimento. Vamos ver como esses stubs se comportam alterando nosso arquivo de testes:

const sinon = require('sinon');
const { expect } = require('chai');
 
const cotacaoComMultiplicador = require('./cotacaoComMultiplicador.js');
const getCotacaoAPI = require('./getCotacaoAPI.js');
 
sinon.stub(getCotacaoAPI, 'getCotacaoAPI').returns(505);
 
describe('cotacaoComMultiplicador', () => {
   it('cotacaoComMultiplicador deve retornar 510.05', () => {
       const resposta = cotacaoComMultiplicador();
       expect(resposta).equal(510.05);
   });
});

Com o stubs, estamos forçando a função `getCotacaoAPI` a retornar o valor 505, que multiplicado por 1,01 dá o nosso esperado número 510,05, o que está correto!

Resultados:

testes_passando

Agora nosso teste unitário está realmente testando o que importa, que é a nossa regra de negócio de aplicar 1% a mais na cotação. Os testes integrados tendem a levar pouquíssimo tempo pelo fato de não fazer requisições externas ou chamadas para banco, como podemos reparar no tempo de execução (2 ms). Com isso cobrimos a parte dos testes unitários. 🚀

Testes integrados

De acordo com a Wikipedia, o `teste de integração` é a fase do teste de software em que os módulos de software individuais são combinados e testados como um grupo.

Extraindo um pouco dessa definição, conseguimos perceber que os testes integrados são testes que completam o ciclo de requisições, são testes que diferentemente do unitário, precisam fazer a requisição a alguma API externa ou fazer uma chamada a um banco de dados.

Então vamos escrever um novo teste para que possamos nos adaptar com as novas regras do jogo.

Lembrando que esse é um arquivo diferente do arquivo de testes unitários.

const sinon = require('sinon');
const { expect } = require('chai');
 
const getCotacaoAPI = require('./getCotacaoAPI.js');
const cotacaoComMultiplicador = require('./cotacaoComMultiplicador.js');
 
const sinonSandbox = sinon.createSandbox();
 
const getCotacaoAPISpy = sinonSandbox.spy(getCotacaoAPI, 'getCotacaoAPI');
 
describe('cotacaoComMultiplicador', () => {
   after(() => {
       sinonSandbox.restore();
   })
   it('cotacaoComMultiplicador deve retornar 510.05', () => {
       const resposta = cotacaoComMultiplicador();
       expect(resposta).equal(getCotacaoAPISpy.getCall(0).returnValue * 1.01);
   });
});

Diferentemente do primeiro teste, aqui introduzimos um conceito de `Spy`. A ideia do Spy é não modificar o jeito que a função é invocada, mas sim termos a possibilidade de realmente espiar as propriedades que essa função carrega. Nesse exemplo, estamos verificando a resposta que a função `getCotacaoAPI` retornou para a função `cotacaoComMultiplicador` e assim podemos verificar corretamente a resposta. Podemos ir um pouco além aqui e adicionar uma expressão para garantir que a função também foi chamada corretamente, assim:

const sinon = require('sinon');
const { expect } = require('chai');
 
const getCotacaoAPI = require('./getCotacaoAPI.js');
const cotacaoComMultiplicador = require('./cotacaoComMultiplicador.js');
 
const sinonSandbox = sinon.createSandbox();
 
const getCotacaoAPISpy = sinonSandbox.spy(getCotacaoAPI, 'getCotacaoAPI');
 
describe('cotacaoComMultiplicador', () => {
   after(() => {
       sinonSandbox.restore();
   })
   it('cotacaoComMultiplicador deve retornar 510.05', () => {
       const resposta = cotacaoComMultiplicador();
       expect(resposta).equal(getCotacaoAPISpy.getCall(0).returnValue * 1.01);
       expect(getCotacaoAPISpy.calledOnce);
   });
});

Testes end-to-end

Esse teste tem como principal objetivo testar o fluxo completo, desde a interação dos cliques na tela até as integrações feitas com seus servidores! A ideia do teste end-to-end é automatizar as ações humanas no seu aplicativo para que você garanta a fidedignidade da sua aplicação.

Testes end-to-end necessitam de uma quantidade maior de configuração e de código, então vou deixar para um artigo separado.

Agora é sua vez!

Faça você mesmo uma aplicação que use testes unitários e integrados, quem sabe isso pode se tornar uma prática na sua carreira. Qualquer dúvida ou comentário que tiver, entre em contato comigo nas minhas redes sociais para que possamos resolver isso juntos.

Linkedin

Github

Twitter

Instagram

Email

Quer ter acesso ao código? Esse é o repositório

Obrigado por ler até aqui, um abraço! o/