Seu carrinho está vazio no momento!

Introdução
Prezados futuros arquitetos de sistemas e engenheiros de software, é uma honra recebê-los nesta que será uma jornada crucial para a excelência das suas APIs. Na Aula 73, mergulharemos em um pilar fundamental do desenvolvimento moderno: o Unit Testing APIs, com foco na validação de funções isoladas. Preparem-se para elevar o patamar de qualidade do seu código!
Para começarmos, imagine que você é um engenheiro de uma prestigiada montadora de automóveis. Antes mesmo de montar o carro completo, cada componente — o motor, o sistema de freios, a suspensão, o sistema de direção — é rigorosamente testado em isolamento. Verificamos se cada peça cumpre sua função específica, sob diversas condições, antes de ser integrada. Isso é um teste unitário no mundo real: assegurar que cada pequena parte do nosso sistema funcione perfeitamente por si só.
No universo das APIs, essa abordagem é simplesmente indispensável. APIs modernas são complexas, com muitas funções lidando com validações, transformações de dados, lógica de negócio e interações. Testar cada uma dessas funções em sua própria “bolha” nos possibilita identificar falhas precocemente, garantir que o comportamento esperado seja mantido e, acima de tudo, construir sistemas robustos e confiáveis. É um investimento que se paga em durabilidade, segurança e paz de espírito.
Nesta sessão, vamos desenvolver e testar funções utilitárias que seriam a espinha dorsal de qualquer API em Node.js com Express. Focaremos em como estruturar nosso código para ser facilmente testável e como empregar ferramentas de ponta para validar cada “unidade” de forma eficaz. Você aprenderá a escrever testes que não apenas verificam a correção, mas também servem como uma excelente documentação viva para o seu código.
No ecossistema Node.js/Express, a testabilidade é um conceito vital. Ao isolar a lógica de negócio em funções puras (que não dependem de estado externo ou efeitos colaterais), e ao desacoplar os manipuladores de rota (route handlers) das suas dependências, você habilita uma estratégia de testes unitários que é rápida, eficiente e altamente recompensadora. Preparem-se para dominar esta arte!
Conceito Fundamental
Caros alunos, o teste unitário, ou Unit Testing, é a prática de validar as menores partes testáveis de uma aplicação de forma isolada. Em nosso contexto de APIs, essas “unidades” são comumente funções ou métodos. O propósito primordial é verificar se cada unidade de código se comporta exatamente como planejado, sem interferências ou dependências de outras partes do sistema.
Vamos detalhar a terminologia correta da indústria que utilizaremos:
- Unidade Sob Teste (UUT – Unit Under Test): Refere-se à função, método ou classe específica que estamos testando em um determinado cenário. É o “componente do motor” que estamos verificando.
- Test Runner: É o software responsável por executar os testes. Ele encontra seus arquivos de teste, roda cada um deles e reporta os resultados. Exemplos notáveis incluem Mocha e Jest.
- Assertion Library (Biblioteca de Asserções): Usada para expressar as expectativas do teste. É onde você declara o que você espera que aconteça com a UUT. Por exemplo, “eu espero que esta função retorne 5” ou “eu espero que esta função lance um erro”. Chai é uma escolha popular e robusta, frequentemente utilizada com Mocha.
- Mocks, Stubs e Spies: São objetos simulados que substituem dependências reais da UUT (como um banco de dados, um serviço de e-mail ou uma API externa). Eles viabilizam o isolamento da UUT, garantindo que o teste não falhe por problemas em componentes externos e que seja rápido e previsível.
Em produção, os casos de uso reais para testes unitários são vastos e significativos. Pense em:
- Validação de Entrada de Dados: Funções que verificam se um e-mail está no formato correto, se uma senha atende aos requisitos de segurança, ou se um número está dentro de um intervalo válido.
- Transformação de Dados: Funções que formatam datas, sanitizam strings para evitar ataques XSS, ou convertem unidades de medida.
- Lógica de Negócio Específica: Cálculos de impostos, algoritmos de recomendação, regras de elegibilidade para descontos ou verificação de permissões.
- Funções de Utilidade Gerais: Qualquer pedaço de código reutilizável que não tenha dependências diretas de estado da aplicação e execute uma tarefa específica.
A integração dos testes unitários com outras tecnologias é relevante. Eles são a primeira linha de defesa em um pipeline de CI/CD (Integração Contínua/Entrega Contínua). Antes que seu código sequer pense em ir para um ambiente de homologação, os testes unitários são executados para garantir que as alterações não introduziram regressões ou novos defeitos. Isso facilita a detecção precoce de problemas, economizando tempo e recursos valiosos.
Como toda ferramenta poderosa, há vantagens e desvantagens:
- Vantagens:
- Detecção Precoce de Bugs: Falhas são encontradas no momento da escrita do código, quando são mais fáceis e baratas de corrigir.
- Refatoração Segura: Permitem que você altere a estrutura do código com a confiança de que os testes o alertarão se algo quebrou.
- Documentação Implícita: Os testes servem como exemplos claros de como a UUT deve ser usada e quais resultados esperar.
- Melhora a Arquitetura do Código: Forçam os desenvolvedores a escreverem código modular, desacoplado e coeso, que é inerentemente mais testável.
- Feedback Rápido: Os testes unitários são geralmente muito rápidos, oferecendo feedback quase instantâneo.
- Desvantagens:
- Custo Inicial de Tempo: Requer um investimento inicial para escrever os testes.
- Falsa Sensação de Segurança: Não garantem que o sistema completo funcione corretamente. Eles precisam ser complementados por testes de integração e ponta a ponta.
- Manutenção de Testes: Testes mal escritos podem se tornar um fardo e precisar de manutenção constante.
Compreender estes conceitos é o primeiro passo para construir APIs de alta qualidade.
Implementação Prática
Agora que dominamos os fundamentos teóricos, vamos sujar as mãos com código! Vamos implementar uma função utilitária e, em seguida, gerar testes unitários robustos para ela. Para isso, utilizaremos as bibliotecas Mocha como nosso test runner e Chai para nossas asserções.
Primeiro, crie uma estrutura de projeto básica para seu Node.js:
mkdir unit-testing-api-aula73
cd unit-testing-api-aula73
npm init -y
npm install mocha chai --save-dev
Altere o package.json para incluir o script de teste:
{
"name": "unit-testing-api-aula73",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "mocha --timeout 5000" // Adiciona o script de teste, com timeout generoso para evitar falhas por lentidão
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"chai": "^4.3.4",
"mocha": "^9.1.3"
}
}
Vamos criar uma função simples que poderia ser vital em uma API: uma função de sanitização de entrada para strings, removendo espaços excessivos e potencialmente caracteres maliciosos para um nome de usuário, por exemplo. Isso é uma melhor prática enterprise para validação e segurança.
Crie o arquivo src/utils/stringSanitizer.js:
// src/utils/stringSanitizer.js
/* @file Contém funções utilitárias para sanitização de strings.
@module StringSanitizer
/
/* Sanitiza uma string, removendo espaços extras e limpando caracteres básicos.
Esta função é um exemplo de uma unidade de código isolada e testável.
@function sanitizeInput
@param {string} input A string de entrada a ser sanitizada.
@returns {string} A string sanitizada.
@throws {Error} Se a entrada não for uma string.
/
function sanitizeInput(input) {
// 1. Logging profissional: É uma boa prática registrar entradas e saídas
// em ambientes de produção para debug e auditoria.
// Para testes unitários, geralmente usamos console.log para depuração interna,
// mas em uma aplicação real, usaria uma lib como Winston ou Pino.
// console.log([sanitizeInput] Recebida entrada: "${input}");
// 2. Validação de entrada robusta: Sempre valide os tipos de entrada.
// Isso previne erros em tempo de execução e torna a função mais previsível.
if (typeof input !== 'string') {
// Lançar um erro é uma forma eficaz de lidar com entradas inválidas.
// É importante que o consumidor da função saiba o que esperar.
throw new Error('A entrada deve ser uma string válida.');
}
// 3. Remove espaços em branco do início e do fim.
let sanitized = input.trim();
// 4. Substitui múltiplos espaços por um único espaço.
sanitized = sanitized.replace(/\s+/g, ' ');
// 5. Remove caracteres que não sejam letras, números, espaços ou hífens/sublinhados.
// Este é um exemplo simples. Em um cenário real, as regras seriam mais complexas
// dependendo do contexto (e.g., nome, email, descrição).
sanitized = sanitized.replace(/[^a-zA-Z0-9\s\-_]/g, '');
// 6. Converte para minúsculas para padronização (opcional, dependendo do caso de uso).
sanitized = sanitized.toLowerCase();
// console.log([sanitizeInput] Saída sanitizada: "${sanitized}");
return sanitized;
}
/ Capitaliza a primeira letra de cada palavra em uma string.
Exemplo de outra função utilitária para testar.
@function capitalizeWords
@param {string} input A string a ser capitalizada.
@returns {string} A string com a primeira letra de cada palavra capitalizada.
@throws {Error} Se a entrada não for uma string.
/
function capitalizeWords(input) {
if (typeof input !== 'string') {
throw new Error('A entrada para capitalizeWords deve ser uma string válida.');
}
return input.toLowerCase().split(' ').map(word => {
return word.charAt(0).toUpperCase() + word.slice(1);
}).join(' ');
}
module.exports = {
sanitizeInput,
capitalizeWords
};
Agora, vamos construir os testes para sanitizeInput e capitalizeWords. Crie o arquivo test/unit/stringSanitizer.test.js:
// test/unit/stringSanitizer.test.js
/ @file Testes unitários para as funções de sanitização de string.
@module StringSanitizerTests
/
// Importa as bibliotecas de teste. 'assert' é uma das interfaces do Chai.
const expect = require('chai').expect;
// Importa a unidade sob teste (Unit Under Test - UUT).
const { sanitizeInput, capitalizeWords } = require('../../src/utils/stringSanitizer');
/* Bloco de testes para a função sanitizeInput.
O 'describe' organiza os testes em grupos lógicos.
/
describe('StringSanitizer - sanitizeInput', () => {
// Teste 1: Cenário de sucesso - entrada básica.
// Cada 'it' descreve um caso de teste específico.
it('deve remover espaços em branco extras do início e do fim e unificar múltiplos espaços', () => {
// AAA Pattern (Arrange-Act-Assert) - Padrão de Melhores Práticas Enterprise
// Arrange: Configura o estado inicial e as entradas.
const input = ' Olá, Mundo! Node.js ';
// Act: Executa a função sob teste.
const result = sanitizeInput(input);
// Assert: Verifica se o resultado é o esperado.
// O Chai's 'expect' torna as asserções muito legíveis.
expect(result).to.equal('olá, mundo! node.js');
});
// Teste 2: Cenário de sucesso - entrada com caracteres especiais permitidos.
it('deve permitir hífens e sublinhados', () => {
const input = 'nome-de_usuario_com-hifen';
const result = sanitizeInput(input);
expect(result).to.equal('nome-de_usuario_com-hifen');
});
// Teste 3: Cenário de sucesso - entrada vazia.
it('deve retornar uma string vazia para entrada vazia ou apenas espaços', () => {
expect(sanitizeInput('')).to.equal('');
expect(sanitizeInput(' ')).to.equal('');
});
// Teste 4: Cenário de sucesso - remoção de caracteres proibidos.
it('deve remover caracteres especiais não permitidos', () => {
const input = 'Nome@Usuário#123!$%^&()';
const result = sanitizeInput(input);
// Esperamos que apenas letras, números e espaços sejam mantidos,
// e que a string seja convertida para minúsculas.
expect(result).to.equal('nomeusuário123');
});
// Teste 5: Cenário de erro - entrada não string.
it('deve lançar um erro se a entrada não for uma string', () => {
// Para testar se uma função lança um erro, envolvemos a chamada em uma função anônima.
// O Chai permite assertar sobre o tipo e a mensagem do erro.
expect(() => sanitizeInput(123)).to.throw('A entrada deve ser uma string válida.');
expect(() => sanitizeInput(null)).to.throw('A entrada deve ser uma string válida.');
expect(() => sanitizeInput(undefined)).to.throw('A entrada deve ser uma string válida.');
expect(() => sanitizeInput({})).to.throw('A entrada deve ser uma string válida.');
});
// Teste 6: Cenário de sucesso - números na string.
it('deve lidar corretamente com números na string', () => {
const input = 'Produto SKU 12345';
const result = sanitizeInput(input);
expect(result).to.equal('produto sku 12345');
});
});
/ Bloco de testes para a função capitalizeWords.
/
describe('StringSanitizer - capitalizeWords', () => {
// Teste 1: Cenário de sucesso - frase comum.
it('deve capitalizar a primeira letra de cada palavra', () => {
const input = 'olá mundo, como vai?';
const result = capitalizeWords(input);
expect(result).to.equal('Olá Mundo, Como Vai?');
});
// Teste 2: Cenário de sucesso - string vazia.
it('deve retornar uma string vazia para entrada vazia', () => {
expect(capitalizeWords('')).to.equal('');
});
// Teste 3: Cenário de sucesso - string já capitalizada.
it('deve lidar corretamente com strings já capitalizadas ou com letras maiúsculas misturadas', () => {
const input = 'IsSo É Um TesTe';
const result = capitalizeWords(input);
expect(result).to.equal('Isso É Um Teste');
});
// Teste 4: Cenário de erro - entrada não string.
it('deve lançar um erro se a entrada não for uma string', () => {
expect(() => capitalizeWords(123)).to.throw('A entrada para capitalizeWords deve ser uma string válida.');
expect(() => capitalizeWords(null)).to.throw('A entrada para capitalizeWords deve ser uma string válida.');
});
});
Para rodar os testes, basta executar no terminal:
npm test
Você verá um relatório detalhado dos testes, indicando quais passaram e quais falharam. Este feedback é valioso para o desenvolvimento ágil.
Melhores Práticas Enterprise:
- Organização de Testes: Mantenha uma estrutura de pastas
test/unitoutest/integrationque espelhe a estruturasrcda sua aplicação. Isso possibilita encontrar os testes relacionados a cada módulo rapidamente. - Padrão AAA (Arrange-Act-Assert): Cada teste deve seguir este padrão: Arrange (configurar o ambiente e dados), Act (executar a UUT), Assert (verificar o resultado). Isso torna os testes claros e fáceis de entender.
- Nomes de Testes Descritivos: Seus nomes de
describeeitdevem ser frases claras que explicam o que está sendo testado e o resultado esperado. - Testes Independentes: Cada teste deve ser independente dos outros. Evite testes que dependam da ordem de execução.
- Testes Rápidos: Testes unitários devem ser executados em milissegundos. Se eles estiverem lentos, provavelmente estão fazendo mais do que deveriam ou interagindo com sistemas externos.
- Cobertura de Código: Busque uma alta cobertura de código, mas não como métrica única. É mais significativo ter testes que validem a lógica crucial e os casos de borda do que 100% de cobertura com testes triviais.
Configurações Específicas para HostGator Plano M:
É importante entender que testes unitários, por sua natureza, são executados no ambiente de desenvolvimento ou em um sistema de CI/CD (Integração Contínua), antes do deploy. No HostGator Plano M, você irá implantar o código da sua aplicação Node.js (os arquivos src/utils/stringSanitizer.js, por exemplo), mas os arquivos de teste (test/unit/...) e as dependências de desenvolvimento (mocha, chai) geralmente não são enviados para o servidor de produção.
O HostGator Plano M oferece um ambiente Node.js padrão. O que importa é que sua aplicação principal esteja otimizada e que o Node.js tenha sido compilado e testado localmente. Seus scripts npm install e npm start (ou node server.js) deverão funcionar normalmente no ambiente do HostGator, desde que você não use recursos ou serviços que não estejam disponíveis lá (como bancos de dados específicos que precisam de configuração extra, mas isso seria para testes de integração/E2E).
Error Handling Fantástico:
Perceba em sanitizeInput e capitalizeWords a validação if (typeof input !== 'string'). Isso é uma validação de entrada robusta. Ao lançar um Error, indicamos claramente que a função não pode operar com o tipo de dado fornecido. Nossos testes validam especificamente este comportamento, assegurando que o tratamento de erro esteja funcionando conforme o planejado. Essa abordagem é essencial para criar APIs resilientes.
Exercício Hands-On
Agora é a sua vez de consolidar este conhecimento! Coloque a mão na massa e desenvolva sua própria função utilitária e seus respectivos testes.
Desafio Prático:
Sua tarefa é criar uma nova função chamada calculateDiscountedPrice(price, discountPercentage) que calcula o preço final de um produto após aplicar um desconto. Além disso, você deve implementar testes unitários abrangentes para esta função.
Requisitos para a função calculateDiscountedPrice:
- Deve aceitar dois parâmetros:
price(número) ediscountPercentage(número). - Deve validar que ambos os parâmetros são números. Caso contrário, deve lançar um erro.
- O
pricedeve ser um número positivo. Se for zero ou negativo, deve lançar um erro. - O
discountPercentagedeve estar entre 0 e 100 (inclusive). Se estiver fora deste intervalo, deve lançar um erro. - Deve retornar o preço com desconto, arredondado para duas casas decimais.
Solução Detalhada Passo a Passo:
Passo 1: Crie o arquivo da função
Crie um novo arquivo src/utils/priceCalculator.js:
// src/utils/priceCalculator.js
/ Calcula o preço de um item após a aplicação de um desconto.
@function calculateDiscountedPrice
@param {number} price O preço original do item.
@param {number} discountPercentage A porcentagem de desconto a ser aplicada (0-100).
@returns {number} O preço final com desconto, arredondado para duas casas decimais.
@throws {Error} Se os parâmetros forem inválidos.
/
function calculateDiscountedPrice(price, discountPercentage) {
// Validação de entrada: Verificamos os tipos dos parâmetros.
if (typeof price !== 'number' || typeof discountPercentage !== 'number') {
throw new Error('Preço e porcentagem de desconto devem ser números.');
}
// Validação de regras de negócio: Preço deve ser positivo.
if (price <= 0) {
throw new Error('O preço deve ser um número positivo.');
}
// Validação de regras de negócio: Porcentagem de desconto deve estar entre 0 e 100.
if (discountPercentage < 0 || discountPercentage > 100) {
throw new Error('A porcentagem de desconto deve estar entre 0 e 100.');
}
const discountAmount = price (discountPercentage / 100);
const finalPrice = price - discountAmount;
// Arredonda para duas casas decimais.
return parseFloat(finalPrice.toFixed(2));
}
module.exports = {
calculateDiscountedPrice
};
Passo 2: Crie o arquivo de teste
Crie um novo arquivo test/unit/priceCalculator.test.js:
// test/unit/priceCalculator.test.js
const expect = require('chai').expect;
const { calculateDiscountedPrice } = require('../../src/utils/priceCalculator');
describe('PriceCalculator - calculateDiscountedPrice', () => {
// Testes de sucesso
it('deve calcular o preço com 10% de desconto corretamente', () => {
expect(calculateDiscountedPrice(100, 10)).to.equal(90);
});
it('deve calcular o preço com 0% de desconto (preço cheio)', () => {
expect(calculateDiscountedPrice(50, 0)).to.equal(50);
});
it('deve calcular o preço com 100% de desconto (preço zero)', () => {
expect(calculateDiscountedPrice(200, 100)).to.equal(0);
});
it('deve lidar com preços e descontos decimais e arredondar para 2 casas', () => {
expect(calculateDiscountedPrice(99.99, 15)).to.equal(84.99); // 99.99 0.85 = 84.9915 -> 84.99
expect(calculateDiscountedPrice(10.50, 5.5)).to.equal(9.92); // 10.50 0.945 = 9.9225 -> 9.92
});
// Testes de erro - Validação de tipos
it('deve lançar um erro se o preço não for um número', () => {
expect(() => calculateDiscountedPrice('abc', 10)).to.throw('Preço e porcentagem de desconto devem ser números.');
expect(() => calculateDiscountedPrice(null, 10)).to.throw('Preço e porcentagem de desconto devem ser números.');
});
it('deve lançar um erro se a porcentagem de desconto não for um número', () => {
expect(() => calculateDiscountedPrice(100, 'dez')).to.throw('Preço e porcentagem de desconto devem ser números.');
expect(() => calculateDiscountedPrice(100, undefined)).to.throw('Preço e porcentagem de desconto devem ser números.');
});
// Testes de erro - Validação de regras de negócio
it('deve lançar um erro se o preço for zero ou negativo', () => {
expect(() => calculateDiscountedPrice(0, 10)).to.throw('O preço deve ser um número positivo.');
expect(() => calculateDiscountedPrice(-10, 10)).to.throw('O preço deve ser um número positivo.');
});
it('deve lançar um erro se a porcentagem de desconto for negativa', () => {
expect(() => calculateDiscountedPrice(100, -5)).to.throw('A porcentagem de desconto deve estar entre 0 e 100.');
});
it('deve lançar um erro se a porcentagem de desconto for maior que 100', () => {
expect(() => calculateDiscountedPrice(100, 101)).to.throw('A porcentagem de desconto deve estar entre 0 e 100.');
});
});
Como Testar e Validar o Resultado:
Para executar os testes para calculateDiscountedPrice, basta ir ao terminal na raiz do seu projeto e rodar novamente:
npm test
Você deverá ver a confirmação de que todos os testes passaram, incluindo os novos que você desenvolveu para calculateDiscountedPrice. Este feedback imediato possibilita a validação da sua lógica e das suas regras de negócio.
Troubleshooting dos Erros Mais Comuns:
AssertionError: Este é o erro mais comum. Significa que o valor retornado pela sua função não corresponde ao que vocêexpect(esperava).- Verifique se o valor esperado no seu teste (
expect(result).to.equal(EXPECTED_VALUE)) está correto. - Depure sua função (
calculateDiscountedPrice) comconsole.logpara ver qual valor ela realmente está retornando. - Atenção a arredondamentos ou tipos de dados (números versus strings).
- Verifique se o valor esperado no seu teste (
Error: Timeout of 5000ms exceeded.: Se um teste demora muito, o Mocha o encerra. Isso geralmente não acontece em testes unitários bem escritos (que são rápidos), mas pode indicar um loop infinito na sua função ou alguma dependência externa real sendo chamada.ReferenceError: expect is not defined: Você provavelmente esqueceu de importarexpectdo Chai ou digitou incorretamente:const expect = require('chai').expect;.Error: Cannot find module '../../src/utils/priceCalculator': Verifique o caminho relativo para o arquivo da sua função. Os caminhos devem estar corretos a partir do seu arquivo de teste.
Próximos Passos Sugeridos:
Esta aula é a fundação para uma arquitetura de testes robusta. Para continuar sua evolução:
- Testes de Integração: Explore como testar a interação entre diferentes unidades (e.g., sua API interagindo com um banco de dados real ou um serviço externo).
- Testes E2E (End-to-End): Aprenda a simular o comportamento de um usuário real em sua aplicação, desde a interface até o banco de dados, utilizando ferramentas como Cypress ou Playwright.
- Integração Contínua (CI/CD): Configure ferramentas como GitHub Actions, GitLab CI/CD ou Jenkins para executar seus testes automaticamente a cada commit de código.
- Cobertura de Testes: Utilize ferramentas como
nyc(Istanbul) com Mocha para medir a porcentagem do seu código coberta por testes. - Outras Bibliotecas: Pesquise sobre
Jestcomo uma alternativa “all-in-one” ao Mocha + Chai, que oferece runner, assertion, mocking e cobertura de código em um único pacote.
Parabéns por dominar esta etapa fundamental! A capacidade de testar seu código com confiança é um diferencial valioso para qualquer desenvolvedor de APIs.
🚀 Pronto para a próxima aula?
Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!