Seu carrinho está vazio no momento!

Introdução (3 min)
Olá, futuro mestre das APIs! Sejam muito bem-vindos à nossa Aula 74. Hoje, vamos desvendar um aspecto crucial do desenvolvimento de APIs modernas: os testes de integração. Imagine que você está montando um complexo sistema de som para um show. Cada instrumento, cada microfone, cada caixa de som é testado individualmente (isso seria um teste unitário). Mas de que adianta se, na hora de tocar, os cabos estão conectados errados, a mesa de som não se comunica com os amplificadores e o som sai uma cacofonia? É aí que entra o teste de integração: ele garante que todos os componentes, embora perfeitos isoladamente, funcionem em perfeita harmonia quando interligados.
No universo das APIs, essa harmonia é absolutamente imprescindível. Uma API não é apenas um conjunto de funções isoladas; ela é uma ponte complexa entre diferentes serviços, bancos de dados e interfaces de usuário. Sem testes de integração robustos, você estaria lançando sua API ao mar sem saber se o barco realmente flutua ou se os remos estão funcionando juntos. Este tipo de validação é vital para assegurar a confiabilidade, a estabilidade e a corretude das suas interações entre módulos, minimizando surpresas desagradáveis em produção.
Nesta aula, nosso objetivo é aplicar, de forma prática e direta, o Supertest, uma biblioteca excepcional para testar requisições HTTP em aplicações Node.js/Express. Vamos aprender a simular interações reais com sua API, verificar respostas e garantir que tudo opere conforme o esperado. Você vai desenvolver a habilidade de construir testes que emulam o comportamento de usuários e sistemas externos, elevando a qualidade do seu código a um patamar verdadeiramente profissional.
No ecossistema Node.js/Express, o Supertest se integra de maneira fluida e elegante. Ele nos permite testar nossa aplicação Express diretamente, sem a necessidade de iniciar um servidor HTTP real e escutar em uma porta. Isso não só agiliza o processo de teste, mas também simplifica a configuração, tornando a escrita de testes de integração uma experiência muito mais agradável e eficiente para desenvolvedores que utilizam esta stack.
Conceito Fundamental (7 min)
Vamos mergulhar no coração técnico dos testes de integração. Em sua essência, um teste de integração verifica as interações entre diferentes partes de um sistema. Enquanto um teste unitário foca em uma única “unidade” de código (como uma função específica ou um método de classe), isolando-a de suas dependências, o teste de integração examina como essas unidades trabalham em conjunto. No contexto de uma API REST, isso geralmente significa testar um endpoint completo: desde a chegada da requisição HTTP, passando por middlewares, lógica de negócio, interação com o banco de dados e a geração da resposta HTTP final.
A terminologia da indústria é clara: estamos falando de validar os fluxos de dados e a comunicação entre módulos. O Supertest é uma ferramenta que facilita essa validação de forma primorosa. Ele atua como um “cliente HTTP” programático, permitindo que você envie requisições (GET, POST, PUT, DELETE, etc.) diretamente para sua aplicação Express, sem que ela precise estar “ouvindo” em uma porta real. Ele se acopla ao seu objeto app do Express e simula a pilha de rede, capturando a resposta gerada. Em seguida, você pode usar assertions (afirmações) para verificar o código de status HTTP (ex: 200 OK, 201 Created, 404 Not Found), os cabeçalhos e o corpo da resposta.
Quais são os casos de uso reais em um ambiente de produção? Pense em uma API de e-commerce:
- Testar se o endpoint
POST /produtosrealmente salva um novo produto no banco de dados e retorna o status201 Created. - Verificar se o endpoint
GET /usuarios/:idretorna os dados corretos de um usuário existente e404 Not Foundpara um ID inexistente. - Assegurar que um endpoint
PUT /pedidos/:idatualiza o status de um pedido corretamente e que a validação de dados de entrada impede valores inválidos. - Validar que middlewares de autenticação e autorização funcionam como esperado, negando acesso a usuários sem permissão ou sem token.
Esses cenários são essenciais para garantir que sua API se comporte de maneira previsível e segura.
A integração do Supertest com outras tecnologias é notável. Ele geralmente é utilizado em conjunto com um test runner como Jest ou Mocha, que fornecem a estrutura para organizar, executar e relatar os resultados dos seus testes. Ao testar interações com bancos de dados, é comum usar um banco de dados em memória (como SQLite para Node.js) ou gerenciar um ambiente de teste dedicado para o banco de dados real, garantindo que cada teste comece com um estado de dados limpo e previsível (um processo conhecido como setup e teardown de dados de teste).
As vantagens são muitas: detecção precoce de falhas de comunicação entre módulos, validação de fluxos de ponta a ponta (excluindo a camada de UI), aumento da confiança na estabilidade da API e uma cobertura de testes que espelha mais de perto o comportamento real do sistema. Isso contribui para um sistema mais robusto e resiliente.
No entanto, há algumas desvantagens a considerar. Testes de integração são inerentemente mais lentos que os testes unitários, pois envolvem mais componentes e, potencialmente, operações de I/O (como acesso a banco de dados). Eles também podem ser mais complexos de configurar e manter, especialmente quando se lida com a gestão do estado do banco de dados entre os testes. Apesar disso, o valor que entregam em termos de segurança e confiabilidade do sistema os torna uma parte inestimável do processo de desenvolvimento de software moderno.
Implementação Prática (10 min)
Chegou a hora de colocar as mãos na massa! Vamos construir uma pequena API Express e, em seguida, escrever testes de integração para ela usando Supertest e Jest. Esta configuração é 100% compatível com um ambiente como o HostGator Plano M, pois os testes são executados localmente e o código Express segue padrões universais.
Primeiro, crie um novo diretório para o seu projeto e inicialize-o:
mkdir api-integration-tests
cd api-integration-tests
npm init -y
Agora, instale as dependências necessárias: Express para a API, Jest como nosso test runner e Supertest para as requisições HTTP nos testes.
npm install express dotenv
npm install --save-dev jest supertest
Precisamos configurar o Jest. Adicione ao seu package.json a seguinte linha dentro do objeto principal:
"scripts": {
"start": "node src/server.js",
"test": "jest --detectOpenHandles"
}
O argumento --detectOpenHandles é útil para depurar testes que deixam handles abertos, como conexões de banco de dados ou servidores HTTP (embora o Supertest minimiza isso). Crie um arquivo jest.config.js na raiz do projeto para configurar o Jest:
// jest.config.js
module.exports = {
testEnvironment: 'node', // Define o ambiente de teste como Node.js
// Padrão para encontrar arquivos de teste (neste caso, .test.js e .spec.js em qualquer pasta src)
testMatch: ['/__tests__//.js', '/?(.)+(spec|test).js'],
// Ignora o diretório node_modules
testPathIgnorePatterns: ['/node_modules/'],
// Roda testes em paralelo para maior velocidade
maxWorkers: '50%',
// Define o tempo limite para cada teste (em ms)
testTimeout: 10000,
// Define que arquivos devem ser coletados para cobertura de código
collectCoverageFrom: [
'src/*/.js',
'!src/server.js', // Exclui o arquivo principal do servidor da cobertura
],
// Limpa mocks antes de cada teste
clearMocks: true,
};
1. Construindo a API (src/app.js e src/server.js)
Crie um diretório src e dentro dele os arquivos app.js e server.js.
src/app.js (Nossa Aplicação Express)
// src/app.js
const express = require('express');
const app = express(); // Instancia a aplicação Express
const morgan = require('morgan'); // Middleware de logging para requisições HTTP
const { v4: uuidv4 } = require('uuid'); // Para gerar IDs únicos (instale com: npm install uuid)
// Para este exemplo, simularemos um banco de dados com um array em memória.
// Em um cenário enterprise, aqui você teria sua conexão com PostgreSQL, MySQL, MongoDB, etc.
let produtos = [
{ id: '1', nome: 'Monitor Gamer', preco: 1500 },
{ id: '2', nome: 'Teclado Mecânico', preco: 400 },
];
// Configuração de middlewares
app.use(express.json()); // Habilita o parsing de JSON no corpo das requisições
app.use(morgan('dev')); // 'dev' é um formato de log conciso para desenvolvimento
// Middleware de logging profissional (exemplo simplificado)
// Em um ambiente de produção, usaria bibliotecas como Winston ou Pino
app.use((req, res, next) => {
console.log([${new Date().toISOString()}] ${req.method} ${req.originalUrl});
next(); // Continua para a próxima função middleware/rota
});
// Rotas da API
// GET /produtos - Retorna todos os produtos
app.get('/produtos', (req, res) => {
console.log('GET /produtos: Retornando todos os produtos.');
res.status(200).json(produtos); // Retorna status 200 OK e a lista de produtos
});
// GET /produtos/:id - Retorna um produto específico pelo ID
app.get('/produtos/:id', (req, res) => {
const { id } = req.params; // Extrai o ID dos parâmetros da URL
const produto = produtos.find(p => p.id === id); // Busca o produto
if (!produto) {
console.warn(GET /produtos/${id}: Produto não encontrado.);
// Retorna status 404 Not Found se o produto não existir
return res.status(404).json({ mensagem: 'Produto não encontrado.' });
}
console.log(GET /produtos/${id}: Retornando produto.);
res.status(200).json(produto); // Retorna status 200 OK e o produto
});
// POST /produtos - Adiciona um novo produto
app.post('/produtos', (req, res) => {
const { nome, preco } = req.body; // Extrai nome e preco do corpo da requisição
// Validação de entrada robusta
if (!nome || typeof nome !== 'string' || !preco || typeof preco !== 'number' || preco <= 0) {
console.error('POST /produtos: Dados de produto inválidos.', req.body);
// Retorna status 400 Bad Request para dados inválidos
return res.status(400).json({ mensagem: 'Dados de produto inválidos. Nome (string) e Preço (número positivo) são obrigatórios.' });
}
const novoProduto = { id: uuidv4(), nome, preco }; // Cria um novo produto com ID único
produtos.push(novoProduto); // Adiciona ao array (nosso "banco de dados")
console.log('POST /produtos: Produto criado com sucesso.', novoProduto);
// Retorna status 201 Created e o novo produto
res.status(201).json(novoProduto);
});
// PUT /produtos/:id - Atualiza um produto existente
app.put('/produtos/:id', (req, res) => {
const { id } = req.params;
const { nome, preco } = req.body;
let produtoIndex = produtos.findIndex(p => p.id === id);
if (produtoIndex === -1) {
console.warn(PUT /produtos/${id}: Produto para atualização não encontrado.);
return res.status(404).json({ mensagem: 'Produto não encontrado para atualização.' });
}
// Validação robusta para atualizações
if ((nome && typeof nome !== 'string') || (preco && (typeof preco !== 'number' || preco <= 0))) {
console.error('PUT /produtos: Dados de atualização inválidos.', req.body);
return res.status(400).json({ mensagem: 'Dados de atualização inválidos. Nome (string) e Preço (número positivo) são opcionais, mas devem ser válidos se presentes.' });
}
// Aplica as atualizações
produtos[produtoIndex] = {
...produtos[produtoIndex],
...(nome && { nome }),
...(preco && { preco })
};
console.log(PUT /produtos/${id}: Produto atualizado com sucesso., produtos[produtoIndex]);
res.status(200).json(produtos[produtoIndex]);
});
// Middleware de tratamento de erros global (melhores práticas enterprise)
app.use((err, req, res, next) => {
console.error('Erro na requisição:', err.stack);
res.status(500).send('Algo deu errado no servidor!');
});
// Exporta a aplicação Express para que possa ser usada nos testes
module.exports = app;
src/server.js (Ponto de entrada da Aplicação)
// src/server.js
const app = require('./app'); // Importa a aplicação Express que definimos
require('dotenv').config(); // Carrega variáveis de ambiente do .env
const PORT = process.env.PORT || 3000; // Define a porta, pegando de variáveis de ambiente ou default 3000
// Inicia o servidor apenas se este arquivo for executado diretamente
// Isso permite que os testes importem app sem iniciar o servidor HTTP real
if (require.main === module) {
app.listen(PORT, () => {
console.log(Servidor rodando na porta ${PORT});
console.log(Ambiente: ${process.env.NODE_ENV || 'development'});
});
}
Crie um arquivo .env na raiz do projeto:
PORT=3001
NODE_ENV=development
Note que o server.js é configurado para não iniciar o servidor se for importado (require.main !== module), o que é vital para o Supertest.
2. Escrevendo os Testes de Integração (__tests__/integration/produtos.test.js)
Crie um diretório __tests__/integration na raiz do projeto e dentro dele o arquivo produtos.test.js.
// __tests__/integration/produtos.test.js
const request = require('supertest'); // Importa o Supertest
const app = require('../../src/app'); // Importa a aplicação Express
// A variável produtos é global na nossa app de exemplo.
// Para testes de integração, é crucial isolar o estado.
// Para fins didáticos, vamos redefinir os produtos a cada teste.
// Em um cenário real, você teria uma função beforeEach que:
// 1. Limpa o banco de dados.
// 2. Popula com dados de teste conhecidos.
let mockProdutos = [];
// Função auxiliar para resetar os dados de teste
const resetProdutos = () => {
mockProdutos = [
{ id: 'prod1', nome: 'Smartphone X', preco: 2500 },
{ id: 'prod2', nome: 'Smart TV 50"', preco: 3500 },
{ id: 'prod3', nome: 'Fones de Ouvido Bluetooth', preco: 300 },
];
// Simulando a atualização dos produtos na app.js
// Em uma aplicação real, você faria um mock ou limparia/populária o BD
const appModule = require('../../src/app');
appModule.produtos = mockProdutos.map(p => ({...p})); // Clonar para evitar mutação direta
};
// Hook beforeEach: Executa antes de CADA teste
beforeEach(() => {
resetProdutos(); // Garante que cada teste comece com um estado limpo e conhecido
});
// Grupo de testes para o endpoint /produtos
describe('API de Produtos - Testes de Integração', () => {
// Teste 1: Deve retornar todos os produtos (GET /produtos)
test('GET /produtos deve retornar todos os produtos com status 200', async () => {
console.log('Executando teste: GET /produtos');
const response = await request(app) // Usa o Supertest com a aplicação Express
.get('/produtos') // Faz uma requisição GET para /produtos
.expect(200); // Espera um status HTTP 200 OK
// Verifica se o corpo da resposta é um array
expect(Array.isArray(response.body)).toBeTruthy();
// Verifica se a quantidade de produtos é a esperada (mockProdutos tem 3)
expect(response.body.length).toBe(3);
// Verifica se um produto específico está presente
expect(response.body).toEqual(expect.arrayContaining([
expect.objectContaining({ id: 'prod1', nome: 'Smartphone X' })
]));
});
// Teste 2: Deve retornar um produto pelo ID (GET /produtos/:id)
test('GET /produtos/:id deve retornar um produto específico com status 200', async () => {
console.log('Executando teste: GET /produtos/:id (existente)');
const produtoId = mockProdutos[0].id;
const response = await request(app)
.get(/produtos/${produtoId}) // Faz GET para um ID específico
.expect(200);
// Verifica o corpo da resposta
expect(response.body).toHaveProperty('id', produtoId);
expect(response.body).toHaveProperty('nome', 'Smartphone X');
expect(response.body).toHaveProperty('preco', 2500);
});
// Teste 3: Deve retornar 404 para produto não encontrado (GET /produtos/:id)
test('GET /produtos/:id deve retornar 404 se o produto não for encontrado', async () => {
console.log('Executando teste: GET /produtos/:id (não existente)');
const produtoIdInexistente = '99999';
const response = await request(app)
.get(/produtos/${produtoIdInexistente})
.expect(404); // Espera status 404 Not Found
expect(response.body).toHaveProperty('mensagem', 'Produto não encontrado.');
});
// Teste 4: Deve criar um novo produto (POST /produtos)
test('POST /produtos deve criar um novo produto com status 201', async () => {
console.log('Executando teste: POST /produtos (sucesso)');
const novoProdutoData = {
nome: 'Mouse Sem Fio',
preco: 150
};
const response = await request(app)
.post('/produtos')
.send(novoProdutoData) // Envia os dados do novo produto no corpo da requisição
.expect(201); // Espera status 201 Created
expect(response.body).toHaveProperty('id'); // Verifica se um ID foi gerado
expect(response.body).toHaveProperty('nome', novoProdutoData.nome);
expect(response.body).toHaveProperty('preco', novoProdutoData.preco);
// Opcional: Verificar se o produto foi realmente adicionado à lista (ao nosso "BD")
// Note: Isso acessa o estado interno da app, o que pode ser considerado menos "black-box"
const appModule = require('../../src/app');
expect(appModule.produtos.length).toBe(mockProdutos.length + 1); // Mock tinha 3, agora deve ter 4
expect(appModule.produtos).toEqual(expect.arrayContaining([
expect.objectContaining({ nome: 'Mouse Sem Fio', preco: 150 })
]));
});
// Teste 5: Deve retornar 400 para dados inválidos na criação de produto (POST /produtos)
test('POST /produtos deve retornar 400 para dados inválidos', async () => {
console.log('Executando teste: POST /produtos (dados inválidos)');
const dadosInvalidos = {
nome: '', // Nome vazio
preco: -10 // Preço negativo
};
const response = await request(app)
.post('/produtos')
.send(dadosInvalidos)
.expect(400); // Espera status 400 Bad Request
expect(response.body).toHaveProperty('mensagem', 'Dados de produto inválidos. Nome (string) e Preço (número positivo) são obrigatórios.');
// Verifica se o número de produtos não mudou
const appModule = require('../../src/app');
expect(appModule.produtos.length).toBe(mockProdutos.length); // Deve continuar com 3 produtos
});
// Teste 6: Deve atualizar um produto existente (PUT /produtos/:id)
test('PUT /produtos/:id deve atualizar um produto existente com status 200', async () => {
console.log('Executando teste: PUT /produtos/:id (sucesso)');
const produtoParaAtualizarId = mockProdutos[0].id; // ID do Smartphone X
const dadosAtualizados = {
nome: 'Smartphone X Pro Max',
preco: 2800
};
const response = await request(app)
.put(/produtos/${produtoParaAtualizarId})
.send(dadosAtualizados)
.expect(200);
expect(response.body).toHaveProperty('id', produtoParaAtualizarId);
expect(response.body).toHaveProperty('nome', 'Smartphone X Pro Max');
expect(response.body).toHaveProperty('preco', 2800);
// Opcional: verificar se o produto foi realmente atualizado na lista
const appModule = require('../../src/app');
const produtoAtualizadoNoDB = appModule.produtos.find(p => p.id === produtoParaAtualizarId);
expect(produtoAtualizadoNoDB).toEqual(expect.objectContaining(dadosAtualizados));
});
// Teste 7: Deve retornar 404 ao tentar atualizar produto não existente (PUT /produtos/:id)
test('PUT /produtos/:id deve retornar 404 para produto não encontrado', async () => {
console.log('Executando teste: PUT /produtos/:id (não existente)');
const produtoIdInexistente = '99999-update';
const dadosAtualizados = { nome: 'Produto Fantasma' };
const response = await request(app)
.put(/produtos/${produtoIdInexistente})
.send(dadosAtualizados)
.expect(404);
expect(response.body).toHaveProperty('mensagem', 'Produto não encontrado para atualização.');
});
// Teste 8: Deve retornar 400 para dados de atualização inválidos (PUT /produtos/:id)
test('PUT /produtos/:id deve retornar 400 para dados de atualização inválidos', async () => {
console.log('Executando teste: PUT /produtos/:id (dados inválidos)');
const produtoParaAtualizarId = mockProdutos[0].id;
const dadosInvalidos = { preco: -50 }; // Preço negativo
const response = await request(app)
.put(/produtos/${produtoParaAtualizarId})
.send(dadosInvalidos)
.expect(400);
expect(response.body).toHaveProperty('mensagem', 'Dados de atualização inválidos. Nome (string) e Preço (número positivo) são opcionais, mas devem ser válidos se presentes.');
});
});
Para rodar os testes:
npm test
Melhores Práticas Enterprise:
- Isolamento de Estado: Para testes de integração, é crucial que cada teste comece em um estado conhecido e limpo. Em uma aplicação real com banco de dados, você implementaria um
beforeEachque limpa o banco de dados e o popula com dados de teste. Para este exemplo, usamos um array global e o redefinimos, mas isso não seria escalável para um sistema complexo. - Variáveis de Ambiente: Use
dotenvpara gerenciar configurações (como a porta da API ou credenciais de banco de dados). Isso é vital para ter ambientes de desenvolvimento, teste e produção com configurações distintas. - Logging Profissional: Embora tenhamos usado
console.logpara simplicidade, em produção, utilize bibliotecas de logging robustas como Winston ou Pino. Integre-as à sua API e verifique os logs em seus testes (seja por arquivos ou capturando saídas). - Validação de Entrada: A API de exemplo já inclui validações básicas. Em um cenário enterprise, utilize schemas de validação como Joi ou Yup para garantir que todos os dados de entrada estejam no formato e tipo corretos, prevenindo vulnerabilidades e erros.
- Tratamento de Erros Centralizado: O middleware de erro global
app.use((err, req, res, next) => { ... })é um padrão enterprise. Ele captura erros que ocorrem em suas rotas e middlewares, garantindo que o servidor sempre retorne uma resposta padronizada e útil (e registre o erro internamente). - Estrutura de Testes: Organize seus testes em diretórios lógicos (ex:
__tests__/unit,__tests__/integration,__tests__/e2e). Isso facilita a navegação e a manutenção.
Configurações Específicas para HostGator Plano M:
O código apresentado é totalmente compatível com o ambiente Node.js do HostGator Plano M (ou qualquer outro ambiente que suporte Node.js). O servidor Express pode ser implantado e executado lá. Os testes de integração, no entanto, são geralmente executados no ambiente de desenvolvimento do programador (localmente ou em um pipeline de CI/CD), e não no servidor de produção da HostGator. Se sua aplicação utilizar um banco de dados real (MySQL/PostgreSQL, que o HostGator geralmente oferece), a conexão na sua API (src/app.js) precisaria ser configurada para apontar para o servidor de banco de dados fornecido pela HostGator, usando variáveis de ambiente. Para os testes, você pode usar um banco de dados em memória para agilizar ou configurar um banco de dados de teste dedicado.
Exercício Hands-On (5 min)
Muito bem, agora é sua vez de brilhar! Vamos expandir a funcionalidade da nossa API e validar essa nova feature com um teste de integração.
Desafio Prático: Implementar e Testar o Endpoint DELETE
Seu desafio é adicionar um novo endpoint na API de produtos para permitir a remoção de produtos, e em seguida, escrever testes de integração para ele.
- Implemente o Endpoint DELETE: Adicione uma rota
DELETE /produtos/:idno seu arquivosrc/app.js. Essa rota deve:- Receber o
iddo produto via parâmetros da URL. - Remover o produto correspondente do array
produtos. - Retornar status
204 No Contentse o produto for removido com sucesso. - Retornar status
404 Not Founde uma mensagem de erro se o produto não for encontrado.
- Receber o
- Escreva Testes de Integração: No arquivo
__tests__/integration/produtos.test.js, crie dois novos testes para o endpointDELETE:- Um teste que verifica a remoção bem-sucedida de um produto, esperando o status
204e confirmando que o produto não está mais na lista. - Um teste que verifica o comportamento ao tentar remover um produto que não existe, esperando o status
404.
- Um teste que verifica a remoção bem-sucedida de um produto, esperando o status
Solução Detalhada Passo a Passo:
1. Atualizando src/app.js:
Adicione a seguinte rota logo após a rota PUT:
// DELETE /produtos/:id - Remove um produto
app.delete('/produtos/:id', (req, res) => {
const { id } = req.params;
const produtoIndex = produtos.findIndex(p => p.id === id);
if (produtoIndex === -1) {
console.warn(DELETE /produtos/${id}: Produto não encontrado para exclusão.);
return res.status(404).json({ mensagem: 'Produto não encontrado para exclusão.' });
}
produtos.splice(produtoIndex, 1); // Remove o produto do array
console.log(DELETE /produtos/${id}: Produto removido com sucesso.);
res.status(204).send(); // Retorna 204 No Content para indicar sucesso sem corpo na resposta
});
2. Atualizando __tests__/integration/produtos.test.js:
Adicione os seguintes testes dentro do bloco describe:
// Teste 9: Deve remover um produto existente (DELETE /produtos/:id)
test('DELETE /produtos/:id deve remover um produto existente com status 204', async () => {
console.log('Executando teste: DELETE /produtos/:id (sucesso)');
const produtoParaRemoverId = mockProdutos[0].id; // ID do Smartphone X
const initialProductCount = mockProdutos.length;
await request(app)
.delete(/produtos/${produtoParaRemoverId})
.expect(204); // Espera status 204 No Content
// Verifica se o produto foi realmente removido da lista
const appModule = require('../../src/app');
expect(appModule.produtos.length).toBe(initialProductCount - 1); // Deve ter um produto a menos
expect(appModule.produtos.find(p => p.id === produtoParaRemoverId)).toBeUndefined(); // Produto não deve existir
});
// Teste 10: Deve retornar 404 ao tentar remover produto não existente (DELETE /produtos/:id)
test('DELETE /produtos/:id deve retornar 404 para produto não encontrado', async () => {
console.log('Executando teste: DELETE /produtos/:id (não existente)');
const produtoIdInexistente = '99999-delete';
const initialProductCount = mockProdutos.length;
const response = await request(app)
.delete(/produtos/${produtoIdInexistente})
.expect(404);
expect(response.body).toHaveProperty('mensagem', 'Produto não encontrado para exclusão.');
// Verifica se o número de produtos não mudou
const appModule = require('../../src/app');
expect(appModule.produtos.length).toBe(initialProductCount); // Deve continuar com o mesmo número de produtos
});
Como Testar e Validar o Resultado:
Para executar os testes e verificar se sua solução está correta, simplesmente abra seu terminal na raiz do projeto e execute o comando:
npm test
Você deverá ver uma saída do Jest indicando que todos os 10 testes foram aprovados, incluindo os dois novos testes para o endpoint DELETE. Se houver falhas, o Jest indicará claramente quais testes falharam e por quê.
Troubleshooting dos Erros Mais Comuns:
expect(received).toBe(expected): Se o status code ou o corpo da resposta não corresponder ao que você espera. Revise a lógica da sua rota na API e a sua asserção no teste.Timeout - Async callback was not invoked within the 5000ms timeout: Indica que um teste assíncrono não finalizou a tempo. Geralmente acontece se você esqueceu umawaitem uma operação assíncrona ou se há um loop infinito/promessa pendente. Aumente otestTimeoutnojest.config.jsse suas operações forem realmente lentas (ex: banco de dados externo real), mas primeiramente, investigue gargalos.ReferenceError: app is not defined: Certifique-se de que você importou corretamente sua aplicação Express no arquivo de teste:const app = require('../../src/app');.- Estado de Teste Sujo: Se um teste falha de forma inconsistente, ou se a ordem dos testes altera os resultados, é um sinal clássico de que você não está limpando ou resetando o estado de seus dados de teste entre as execuções. O
beforeEach(resetProdutos)é a solução para isso em nosso exemplo.
Próximos Passos Sugeridos:
- Integração de Autenticação: Adicione um middleware de autenticação (ex: JWT) à sua API e escreva testes de integração para garantir que endpoints protegidos retornem
401 Unauthorizedsem um token válido e funcionem com um token válido. - Validação de Dados Aprimorada: Implemente uma biblioteca de validação como Joi ou Yup na sua API e adicione testes de integração para verificar todos os cenários de dados de entrada válidos e inválidos em diferentes endpoints.
- Conexão com Banco de Dados Real: Configure seu ambiente de teste para usar um banco de dados real (ex: PostgreSQL com Sequelize ou Prisma) e refatore seus testes para limpar e popular o banco de dados antes de cada teste, garantindo um ambiente isolado e consistente.
- Testes de Performance: Explore ferramentas como Artillery ou k6 para realizar testes de carga e estresse na sua API, complementando os testes funcionais que você aprendeu hoje.
Parabéns por completar esta aula! Você agora possui uma base sólida em testes de integração com Supertest, uma habilidade valiosa que o diferenciará no mercado.
🚀 Pronto para a próxima aula?
Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!