Seu carrinho está vazio no momento!

Introdução (3 min)
Imagine que você é um explorador. Antes de lançar seu novo mapa de uma cidade misteriosa para o mundo, você quer ter certeza de que percorreu todas as ruas, visitou todos os pontos turísticos e anotou cada detalhe. Você não quer que seu público descubra uma rua esquecida ou um monumento não mapeado. No mundo do desenvolvimento de software, especialmente com APIs modernas, essa “exploração completa” é exatamente o que chamamos de Cobertura de Testes (Test Coverage).
Para APIs robustas e de alto desempenho, é absolutamente vital garantir que a maior parte do seu código seja executada e verificada por testes automatizados. Isso é mais do que apenas ter testes; é sobre ter uma confiança verificável de que seu código-base é sólido, que as funcionalidades esperadas estão presentes e que as regressões são minimizadas. É a garantia de que, ao fazer uma alteração, você não quebra algo inesperado.
Nesta aula, você vai compreender de forma aprofundada o conceito de cobertura de testes, por que ele é tão relevante e como ferramentas poderosas como o istanbul e o c8 nos possibilitam medir e analisar essa cobertura em seus projetos Node.js e Express. Entenderemos como integrar essas ferramentas para entregar APIs de qualidade superior.
No ecossistema Node.js/Express, onde a agilidade e a manutenção contínua são diferenciais, a cobertura de testes é um pilar. Ela se integra perfeitamente aos fluxos de desenvolvimento e entrega contínua (CI/CD), agindo como um guardião da qualidade do seu código de back-end.
Conceito Fundamental (7 min)
A Cobertura de Testes (ou Code Coverage) é uma métrica que indica a porcentagem do seu código-fonte que é executada quando você executa seus testes automatizados. É uma ferramenta de análise estática e dinâmica que visa quantificar o quão “bem testado” seu software está, fornecendo uma visão clara das áreas que ainda precisam de mais atenção.
Existem diferentes tipos de métricas de cobertura, cada uma com sua perspectiva valiosa:
- Cobertura de Linhas (Line Coverage): Mede a porcentagem de linhas de código executadas pelos testes. É o tipo mais comum e intuitivo.
- Cobertura de Declarações (Statement Coverage): Similar à linha, foca em cada “declaração” (instrução) do código que foi executada.
- Cobertura de Funções (Function Coverage): Indica a porcentagem de funções ou métodos que foram chamados durante a execução dos testes.
- Cobertura de Ramificações (Branch Coverage): Esta é mais avançada e essencial. Garante que cada “ramificação” de decisão (como o
ifemif/else, oucaseemswitch) tenha sido percorrida em todos os seus caminhos possíveis (verdadeiro e falso).
Ferramentas como o istanbul (através de seu CLI, nyc) e o c8 (uma alternativa mais moderna que tira proveito do motor V8 do Node.js) são os protagonistas aqui. Elas instrumentam seu código (adicionam “ganchos” invisíveis) antes da execução dos testes e, após os testes, coletam dados sobre quais partes do código foram atingidas, gerando relatórios detalhados.
Em ambientes de produção, a cobertura de testes é indispensável para:
- Projetos grandes e complexos com múltiplas equipes.
- Sistemas críticos onde falhas são inaceitáveis (saúde, finanças).
- Projetos com alta rotatividade de desenvolvedores.
- Pipelines de CI/CD para impor limites mínimos de cobertura antes do deploy.
Ela se integra a frameworks de teste como Jest, Mocha e Cypress, tornando a análise de cobertura parte natural do ciclo de desenvolvimento.
As vantagens de adotar uma boa cobertura de testes são numerosas:
- Aumento da Confiança: Você e sua equipe podem refatorar e adicionar funcionalidades com mais segurança, sabendo que os testes alertarão sobre quebras.
- Identificação de Código Morto: Ajuda a localizar partes do código que nunca são executadas e podem ser removidas.
- Melhora na Qualidade do Código: Incentiva a escrita de código mais modular e testável.
- Redução de Bugs: Diminui a probabilidade de erros chegarem em produção.
Por outro lado, algumas desvantagens incluem:
- Falsa Sensação de Segurança: Uma alta porcentagem não garante que o código está correto logicamente, apenas que foi executado.
- Custo de Manutenção: Manter testes e sua cobertura atualizados demanda tempo e esforço.
- Não Substitui Testes Manuais/Exploratórios: Não captura a experiência do usuário ou cenários complexos que testes automatizados podem perder.
Implementação Prática (10 min)
Vamos desenvolver um pequeno projeto Node.js/Express e integrar o c8 para medir a cobertura de testes. Escolhemos c8 por ser mais moderno e aproveitar os recursos nativos de cobertura do V8, o motor JavaScript do Node.js.
Primeiro, vamos configurar o projeto:
Estrutura do Projeto
Crie a pasta do projeto
📚 Informações da Aula
Curso: API Completo - Node.js & Express
Tempo estimado: 25 minutos
Pré-requisitos: JavaScript básico
mkdir api-cobertura && cd api-cobertura
Inicialize o Node.js e instale as dependências
npm init -y
npm install express jest c8 winston express-validator
Crie a estrutura de diretórios
mkdir src src/controllers src/services src/routes src/middlewares tests
touch src/app.js src/controllers/usuarioController.js src/services/usuarioService.js src/routes/usuarioRoutes.js src/middlewares/validacao.js tests/usuarioService.test.js .env
Agora, o código. Vamos implementar um serviço simples de usuário, um controlador, rotas, um middleware de validação e um logger.
src/middlewares/validacao.js
Um middleware para validação de entrada robusta.
// src/middlewares/validacao.js
const { validationResult } = require('express-validator');
/* Middleware para validar resultados de requisições.
@param {Object} req - Objeto de requisição do Express.
@param {Object} res - Objeto de resposta do Express.
@param {Function} next - Função para passar para o próximo middleware.
/
function validarRequisicao(req, res, next) {
// Captura os erros de validação da requisição
const erros = validationResult(req);
// Se houver erros, retorna uma resposta 400 com os detalhes
if (!erros.isEmpty()) {
return res.status(400).json({ erros: erros.array() });
}
// Se não houver erros, prossegue para o próximo middleware/rota
next();
}
module.exports = {
validarRequisicao
};
src/services/usuarioService.js
Um serviço que simula a manipulação de usuários.
// src/services/usuarioService.js
// Simulação de um "banco de dados" em memória
let usuarios = [];
let proximoId = 1;
/ @typedef {Object} Usuario
@property {number} id - ID único do usuário.
@property {string} nome - Nome do usuário.
@property {string} email - Email do usuário (deve ser único).
/
/* Retorna todos os usuários registrados.
@returns {Array} Lista de usuários.
/
function obterTodosUsuarios() {
return usuarios;
}
/* Busca um usuário pelo seu ID.
@param {number} id - ID do usuário.
@returns {Usuario|undefined} O usuário encontrado ou undefined.
/
function obterUsuarioPorId(id) {
// Procura um usuário no array pelo ID fornecido
return usuarios.find(u => u.id === id);
}
/ Adiciona um novo usuário.
@param {string} nome - Nome do novo usuário.
@param {string} email - Email do novo usuário.
@returns {Usuario|null} O usuário criado ou null se o email já existir.
/
function criarUsuario(nome, email) {
// Verifica se já existe um usuário com o mesmo email para garantir unicidade
if (usuarios.some(u => u.email === email)) {
return null; // Email já existe, não permite criar
}
// Cria um novo objeto de usuário com um ID único e o adiciona ao array
const novoUsuario = { id: proximoId++, nome, email };
usuarios.push(novoUsuario);
return novoUsuario;
}
/* Atualiza um usuário existente.
@param {number} id - ID do usuário a ser atualizado.
@param {string} nome - Novo nome do usuário.
@param {string} email - Novo email do usuário.
@returns {Usuario|null} O usuário atualizado ou null se não encontrado ou email já existir.
/
function atualizarUsuario(id, nome, email) {
// Encontra o índice do usuário pelo ID
const index = usuarios.findIndex(u => u.id === id);
if (index === -1) {
return null; // Usuário não encontrado
}
// Verifica se o novo email já existe para outro usuário
if (usuarios.some(u => u.email === email && u.id !== id)) {
return null; // Email já existe para outro usuário
}
// Atualiza os dados do usuário
usuarios[index] = { ...usuarios[index], nome, email };
return usuarios[index];
}
/ Exclui um usuário.
@param {number} id - ID do usuário a ser excluído.
@returns {boolean} True se o usuário foi excluído, false caso contrário.
/
function excluirUsuario(id) {
// Filtra o array removendo o usuário com o ID fornecido
const usuariosAntes = usuarios.length;
usuarios = usuarios.filter(u => u.id !== id);
// Retorna true se o número de usuários diminuiu, indicando que um foi removido
return usuarios.length < usuariosAntes;
}
// Exporta as funções do serviço para serem usadas por outros módulos
module.exports = {
obterTodosUsuarios,
obterUsuarioPorId,
criarUsuario,
atualizarUsuario,
excluirUsuario
};
src/controllers/usuarioController.js
O controlador que interage com o serviço e processa requisições HTTP.
// src/controllers/usuarioController.js
const usuarioService = require('../services/usuarioService');
const logger = require('winston'); // Usando winston para logging profissional
// Configuração básica do logger (pode ser mais complexa em produção)
logger.configure({
level: 'info',
format: logger.format.combine(
logger.format.colorize(),
logger.format.simple()
),
transports: [
new logger.transports.Console()
]
});
/ Manipula a requisição para obter todos os usuários.
/
function listarUsuarios(req, res) {
logger.info('GET /usuarios - Listando todos os usuários');
// Chama o serviço para obter a lista de usuários
const usuarios = usuarioService.obterTodosUsuarios();
res.status(200).json(usuarios);
}
/ Manipula a requisição para obter um usuário por ID.
/
function buscarUsuarioPorId(req, res) {
const { id } = req.params;
const userId = parseInt(id, 10); // Converte o ID da URL para número
logger.info(GET /usuarios/${id} - Buscando usuário por ID);
// Verifica se o ID é um número válido
if (isNaN(userId)) {
logger.warn(GET /usuarios/${id} - ID inválido recebido: ${id});
return res.status(400).json({ mensagem: 'ID de usuário inválido.' });
}
// Chama o serviço para buscar o usuário
const usuario = usuarioService.obterUsuarioPorId(userId);
// Retorna o usuário se encontrado, ou 404 caso contrário
if (usuario) {
res.status(200).json(usuario);
} else {
logger.info(GET /usuarios/${id} - Usuário não encontrado.);
res.status(404).json({ mensagem: 'Usuário não encontrado.' });
}
}
/ Manipula a requisição para criar um novo usuário.
/
function criarNovoUsuario(req, res) {
const { nome, email } = req.body; // Pega nome e email do corpo da requisição
logger.info(POST /usuarios - Tentando criar novo usuário: ${email});
// Chama o serviço para criar o usuário
const novoUsuario = usuarioService.criarUsuario(nome, email);
// Retorna 201 (Created) se sucesso, ou 409 (Conflict) se email já existe
if (novoUsuario) {
logger.info(POST /usuarios - Usuário criado com sucesso: ${novoUsuario.id});
res.status(201).json(novoUsuario);
} else {
logger.warn(POST /usuarios - Tentativa de criar usuário com email duplicado: ${email});
res.status(409).json({ mensagem: 'Email já cadastrado.' });
}
}
/ Manipula a requisição para atualizar um usuário existente.
/
function atualizarDadosUsuario(req, res) {
const { id } = req.params;
const userId = parseInt(id, 10);
const { nome, email } = req.body;
logger.info(PUT /usuarios/${id} - Tentando atualizar usuário: ${email});
// Verifica se o ID é um número válido
if (isNaN(userId)) {
logger.warn(PUT /usuarios/${id} - ID inválido recebido: ${id});
return res.status(400).json({ mensagem: 'ID de usuário inválido.' });
}
// Chama o serviço para atualizar o usuário
const usuarioAtualizado = usuarioService.atualizarUsuario(userId, nome, email);
// Retorna 200 (OK) se sucesso, 404 se não encontrado, 409 se email duplicado
if (usuarioAtualizado) {
logger.info(PUT /usuarios/${id} - Usuário atualizado com sucesso: ${usuarioAtualizado.id});
res.status(200).json(usuarioAtualizado);
} else {
// Precisa verificar o motivo pelo qual o serviço retornou null
const usuarioExistente = usuarioService.obterUsuarioPorId(userId);
if (!usuarioExistente) {
logger.warn(PUT /usuarios/${id} - Usuário não encontrado para atualização.);
res.status(404).json({ mensagem: 'Usuário não encontrado.' });
} else {
logger.warn(PUT /usuarios/${id} - Tentativa de atualização com email duplicado: ${email});
res.status(409).json({ mensagem: 'Email já cadastrado para outro usuário.' });
}
}
}
/ Manipula a requisição para excluir um usuário.
/
function removerUsuario(req, res) {
const { id } = req.params;
const userId = parseInt(id, 10);
logger.info(DELETE /usuarios/${id} - Tentando remover usuário);
// Verifica se o ID é um número válido
if (isNaN(userId)) {
logger.warn(DELETE /usuarios/${id} - ID inválido recebido: ${id});
return res.status(400).json({ mensagem: 'ID de usuário inválido.' });
}
// Chama o serviço para excluir o usuário
const excluido = usuarioService.excluirUsuario(userId);
// Retorna 204 (No Content) se sucesso, ou 404 se não encontrado
if (excluido) {
logger.info(DELETE /usuarios/${id} - Usuário removido com sucesso.);
res.status(204).send(); // 204 indica sucesso sem conteúdo de retorno
} else {
logger.warn(DELETE /usuarios/${id} - Usuário não encontrado para remoção.);
res.status(404).json({ mensagem: 'Usuário não encontrado.' });
}
}
module.exports = {
listarUsuarios,
buscarUsuarioPorId,
criarNovoUsuario,
atualizarDadosUsuario,
removerUsuario
};
src/routes/usuarioRoutes.js
Definição das rotas da API para usuários.
// src/routes/usuarioRoutes.js
const express = require('express');
const { body, param } = require('express-validator');
const usuarioController = require('../controllers/usuarioController');
const { validarRequisicao } = require('../middlewares/validacao');
const router = express.Router();
// Rota para listar todos os usuários
router.get('/', usuarioController.listarUsuarios);
// Rota para buscar um usuário por ID, com validação de parâmetro
router.get('/:id',
[
param('id').isInt({ gt: 0 }).withMessage('ID deve ser um número inteiro positivo.')
],
validarRequisicao,
usuarioController.buscarUsuarioPorId
);
// Rota para criar um novo usuário, com validação do corpo da requisição
router.post('/',
[
body('nome').isLength({ min: 3 }).withMessage('Nome deve ter no mínimo 3 caracteres.'),
body('email').isEmail().withMessage('Email inválido.')
],
validarRequisicao,
usuarioController.criarNovoUsuario
);
// Rota para atualizar um usuário existente, com validações
router.put('/:id',
[
param('id').isInt({ gt: 0 }).withMessage('ID deve ser um número inteiro positivo.'),
body('nome').isLength({ min: 3 }).withMessage('Nome deve ter no mínimo 3 caracteres.'),
body('email').isEmail().withMessage('Email inválido.')
],
validarRequisicao,
usuarioController.atualizarDadosUsuario
);
// Rota para excluir um usuário, com validação de parâmetro
router.delete('/:id',
[
param('id').isInt({ gt: 0 }).withMessage('ID deve ser um número inteiro positivo.')
],
validarRequisicao,
usuarioController.removerUsuario
);
module.exports = router;
src/app.js
O arquivo principal da aplicação Express.
// src/app.js
const express = require('express');
const usuarioRoutes = require('./routes/usuarioRoutes');
const logger = require('winston'); // Reutilizando o logger configurado no controller
// Configuração básica do logger (garantindo que esteja configurado para app.js também)
// Em um projeto real, você teria uma única configuração de logger em um arquivo separado.
logger.configure({
level: 'info',
format: logger.format.combine(
logger.format.colorize(),
logger.format.simple()
),
transports: [
new logger.transports.Console()
]
});
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware para parsear JSON do corpo da requisição
app.use(express.json());
// Middlewares de log de requisições - boa prática enterprise
app.use((req, res, next) => {
logger.info(${req.method} ${req.originalUrl});
next();
});
// Rotas da API para usuários
app.use('/usuarios', usuarioRoutes);
// Middleware para lidar com rotas não encontradas (404)
app.use((req, res, next) => {
logger.warn(404 Not Found: ${req.originalUrl});
res.status(404).json({ mensagem: 'Rota não encontrada.' });
});
// Middleware de tratamento de erros global (Error Handling poderoso)
app.use((err, req, res, next) => {
logger.error(Erro interno do servidor: ${err.message}, err);
res.status(500).json({ mensagem: 'Ocorreu um erro interno no servidor.', erro: err.message });
});
// Inicia o servidor apenas se o módulo não for importado (para testes)
if (require.main === module) {
app.listen(PORT, () => {
logger.info(Servidor rodando na porta ${PORT});
});
}
module.exports = app; // Exporta o app para que os testes possam utilizá-lo
tests/usuarioService.test.js
Testes unitários para o serviço de usuário.
// tests/usuarioService.test.js
const usuarioService = require('../src/services/usuarioService');
// Hook para limpar os usuários antes de cada teste
// Isso garante que cada teste comece com um estado limpo
beforeEach(() => {
// Acessa uma propriedade "secreta" do serviço para resetar o estado interno
// Em um cenário real, você exporia uma função reset ou usaria mocks.
usuarioService.obterTodosUsuarios().length = 0; // Limpa o array
let idCounter = 1; // Reseta o contador de ID (hackish, para exemplo)
Object.defineProperty(usuarioService, 'proximoId', {
get: () => idCounter,
set: (value) => { idCounter = value; },
configurable: true
});
usuarioService.proximoId = 1;
});
describe('usuarioService', () => {
test('deve criar um novo usuário', () => {
const usuario = usuarioService.criarUsuario('Alice', '[email protected]');
expect(usuario).toEqual({ id: 1, nome: 'Alice', email: '[email protected]' });
expect(usuarioService.obterTodosUsuarios()).toHaveLength(1);
});
test('não deve criar usuário com email duplicado', () => {
usuarioService.criarUsuario('Bob', '[email protected]');
const usuarioDuplicado = usuarioService.criarUsuario('Carlos', '[email protected]');
expect(usuarioDuplicado).toBeNull();
expect(usuarioService.obterTodosUsuarios()).toHaveLength(1);
});
test('deve obter todos os usuários', () => {
usuarioService.criarUsuario('Davi', '[email protected]');
usuarioService.criarUsuario('Eva', '[email protected]');
const usuarios = usuarioService.obterTodosUsuarios();
expect(usuarios).toHaveLength(2);
expect(usuarios[0].nome).toBe('Davi');
expect(usuarios[1].nome).toBe('Eva');
});
test('deve obter um usuário por ID', () => {
usuarioService.criarUsuario('Frank', '[email protected]');
const usuario = usuarioService.obterUsuarioPorId(1);
expect(usuario).toEqual({ id: 1, nome: 'Frank', email: '[email protected]' });
});
test('deve retornar undefined se o usuário não for encontrado por ID', () => {
const usuario = usuarioService.obterUsuarioPorId(99);
expect(usuario).toBeUndefined();
});
test('deve atualizar um usuário existente', () => {
usuarioService.criarUsuario('Grace', '[email protected]');
const usuarioAtualizado = usuarioService.atualizarUsuario(1, 'Grace Hopper', '[email protected]');
expect(usuarioAtualizado).toEqual({ id: 1, nome: 'Grace Hopper', email: '[email protected]' });
expect(usuarioService.obterUsuarioPorId(1).nome).toBe('Grace Hopper');
});
test('não deve atualizar usuário se ID não encontrado', () => {
const usuarioAtualizado = usuarioService.atualizarUsuario(99, 'Ines', '[email protected]');
expect(usuarioAtualizado).toBeNull();
});
test('não deve atualizar usuário com email que já existe em outro usuário', () => {
usuarioService.criarUsuario('Heitor', '[email protected]'); // ID 1
usuarioService.criarUsuario('Igor', '[email protected]'); // ID 2
const usuarioAtualizado = usuarioService.atualizarUsuario(1, 'Heitor Silva', '[email protected]');
expect(usuarioAtualizado).toBeNull();
expect(usuarioService.obterUsuarioPorId(1).email).toBe('[email protected]'); // Garante que o email original não foi alterado
});
test('deve excluir um usuário', () => {
usuarioService.criarUsuario('Julia', '[email protected]');
const excluido = usuarioService.excluirUsuario(1);
expect(excluido).toBe(true);
expect(usuarioService.obterTodosUsuarios()).toHaveLength(0);
});
test('não deve excluir usuário se ID não encontrado', () => {
usuarioService.criarUsuario('Karen', '[email protected]');
const excluido = usuarioService.excluirUsuario(99);
expect(excluido).toBe(false);
expect(usuarioService.obterTodosUsuarios()).toHaveLength(1);
});
});
Configuração do package.json
Adicione os scripts para rodar os testes e gerar o relatório de cobertura.
// package.json (apenas a seção de scripts e dependências)
{
"name": "api-cobertura",
"version": "1.0.0",
"description": "Exemplo de API Node.js com cobertura de testes usando c8 e Jest.",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"test": "jest",
"coverage": "c8 jest --reporter=text --reporter=html"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.19.2",
"express-validator": "^7.1.0",
"winston": "^3.13.0"
},
"devDependencies": {
"c8": "^9.1.0",
"jest": "^29.7.0"
}
}
Executando a Cobertura
Para gerar o relatório de cobertura, execute o seguinte comando no terminal:
npm run coverage
Este comando irá:
- Executar o
c8, que instrumenta seu código. - Rodar o
jest, que executa seus testes. - Coletar os dados de execução gerados pelo V8.
- Gerar um relatório no console (
--reporter=text) e um relatório HTML interativo na pastacoverage(--reporter=html).
Você pode abrir o arquivo coverage/index.html no seu navegador para ver um relatório detalhado, linha por linha, de qual código foi coberto pelos seus testes.
Melhores Práticas Enterprise:
- Thresholds (Limiares): No
package.jsonou em um arquivo de configuração.c8rc.json, você pode definir limites mínimos de cobertura. Se a cobertura cair abaixo desses limites, o build falhará, impedindo que código mal testado seja enviado.// Exemplo no package.json // "c8": { // "lines": 80, // "functions": 80, // "branches": 80, // "statements": 80, // "check-coverage": true // Isso habilita a verificação dos thresholds // } - Ignorar Arquivos: Use a propriedade
--excludeouexcludenoc8para ignorar arquivos que você não precisa testar (ex: arquivos de configuração,index.jsque apenas exporta outros módulos). - Integração CI/CD: Adicione o comando
npm run coverageao seu pipeline de CI/CD para que cada push ou pull request seja validado quanto à cobertura de testes.
Compatibilidade com HostGator Plano M:
As ferramentas de cobertura de teste (c8, jest) são executadas no ambiente de desenvolvimento ou em um servidor de CI/CD. O código gerado para produção (seu src/app.js e dependências) é que será implantado no HostGator. Uma vez que o HostGator Plano M suporta Node.js, e o código aqui está em padrões ES5/CommonJS, não há impedimentos de compatibilidade. A cobertura é uma métrica de qualidade do seu processo de desenvolvimento, não algo que roda ativamente no servidor de produção.
Exercício Hands-On (5 min)
Desafio Prático
O serviço usuarioService.js está bem coberto, mas nosso usuarioController.js ainda possui lógicas não alcançadas, principalmente nos blocos de erro e cenários específicos. Seu desafio é implementar testes de integração para o usuarioController.js (usando o app.js exportado) para aumentar a cobertura, especialmente nas seguintes partes:
- Testar o endpoint
GET /usuarios/:idquando o ID é inválido (não numérico). - Testar o endpoint
PUT /usuarios/:idquando o ID é válido, mas o usuário não existe. - Testar o middleware de validação (
validarRequisicao) retornando erros (ex: criar usuário com email inválido). - Testar o middleware de rota não encontrada (404).
Solução Detalhada Passo a Passo
Vamos criar um novo arquivo de teste tests/usuarioController.test.js para os testes de integração.
1. Crie tests/usuarioController.test.js
// tests/usuarioController.test.js
const request = require('supertest'); // Instale 'supertest' para testes de integração HTTP: npm install supertest
const app = require('../src/app'); // Importa a aplicação Express
const usuarioService = require('../src/services/usuarioService'); // Para resetar o estado
// Resetar os usuários do serviço antes de cada teste
beforeEach(() => {
usuarioService.obterTodosUsuarios().length = 0;
// Resetar o ID sequencial no serviço (manter consistência com os testes de serviço)
Object.defineProperty(usuarioService, 'proximoId', {
get: () => 1, // Fixa o próximo ID para 1 em cada teste de controller
set: (value) => {}, // Torna o setter no-op para este teste
configurable: true
});
});
describe('Usuario API - Integration Tests', () => {
// Teste 1: GET /usuarios/:id com ID inválido
test('GET /usuarios/:id deve retornar 400 para ID inválido', async () => {
const res = await request(app).get('/usuarios/abc');
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('mensagem', 'ID de usuário inválido.');
});
// Teste 2: PUT /usuarios/:id com ID válido mas usuário inexistente
test('PUT /usuarios/:id deve retornar 404 se usuário não encontrado', async () => {
const res = await request(app)
.put('/usuarios/999')
.send({ nome: 'Inexistente', email: '[email protected]' });
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('mensagem', 'Usuário não encontrado.');
});
// Teste 3: POST /usuarios com email inválido (testando middleware de validação)
test('POST /usuarios deve retornar 400 para email inválido', async () => {
const res = await request(app)
.post('/usuarios')
.send({ nome: 'Valid Name', email: 'invalid-email' });
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('erros');
expect(res.body.erros[0].msg).toBe('Email inválido.');
});
// Teste 4: Middleware de rota não encontrada (404)
test('Deve retornar 404 para rota inexistente', async () => {
const res = await request(app).get('/rota-inexistente');
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('mensagem', 'Rota não encontrada.');
});
// Adicionando alguns testes para cobrir outras rotas também
test('GET /usuarios deve retornar lista vazia se nenhum usuário', async () => {
const res = await request(app).get('/usuarios');
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual([]);
});
test('POST /usuarios deve criar um usuário com sucesso', async () => {
const res = await request(app)
.post('/usuarios')
.send({ nome: 'Test User', email: '[email protected]' });
expect(res.statusCode).toEqual(201);
expect(res.body.nome).toBe('Test User');
expect(res.body.email).toBe('[email protected]');
expect(usuarioService.obterTodosUsuarios()).toHaveLength(1);
});
test('DELETE /usuarios/:id deve retornar 204 se usuário existe e é removido', async () => {
usuarioService.criarUsuario('Delete Me', '[email protected]'); // Cria um usuário com ID 1
const res = await request(app).delete('/usuarios/1');
expect(res.statusCode).toEqual(204);
expect(usuarioService.obterTodosUsuarios()).toHaveLength(0);
});
});
Instale a dependência supertest se ainda não o fez:
npm install supertest --save-dev
2. Execute a Cobertura Novamente
Após adicionar o novo arquivo de teste, execute o comando de cobertura novamente:
npm run coverage
Como Testar e Validar o Resultado
Abra o relatório HTML em coverage/index.html no seu navegador. Navegue pelos arquivos src/controllers/usuarioController.js e src/middlewares/validacao.js. Você deverá notar um aumento nas linhas, funções e ramificações cobertas, especialmente nos blocos if/else que tratam de IDs inválidos, usuários não encontrados ou erros de validação.
Troubleshooting dos Erros Mais Comuns
- “Module not found: supertest”: Certifique-se de ter instalado
supertestcomnpm install supertest --save-dev. - Cobertura não muda: Verifique se seus novos arquivos de teste estão sendo descobertos pelo Jest. Por padrão, Jest procura arquivos
.test.jsou.spec.js. Certifique-se de que o caminho está correto. - Erros de porta: Se você estiver rodando o servidor Express em segundo plano, os testes podem falhar ao tentar iniciar outro servidor na mesma porta. O código do
app.jsfoi ajustado para exportar oappe não iniciar olistense for importado, o que é a maneira correta para testes. - Cobertura 100% falsa: Lembre-se, 100% de cobertura não significa que seu código está impecável. Significa apenas que cada linha foi executada. Você ainda precisa de testes que validem a lógica de negócio.
Próximos Passos Sugeridos
- Integração CI/CD: Configure seu repositório (GitHub Actions, GitLab CI, Jenkins) para rodar
npm run coverageem cada push e, idealmente, falhar se a cobertura cair abaixo de um limite predefinido. - Mutation Testing: Ferramentas como o Stryker.js levam a cobertura de testes um passo adiante, modificando seu código-fonte para ver se seus testes conseguem “pegar” essas mutações, avaliando a qualidade* dos seus testes, não apenas a cobertura.
- Testes de Componente/E2E: Além dos testes unitários e de integração que fizemos, considere testes que simulem a interação completa do usuário com sua API (End-to-End) usando ferramentas como Cypress ou Playwright, especialmente para fluxos críticos.
Parabéns! Você agora domina os fundamentos da cobertura de testes e sabe como aproveitar o c8 para garantir a qualidade e a robustez das suas APIs Node.js/Express. Continue explorando e aprimorando suas habilidades para desenvolver sistemas cada vez mais confiáveis!
🚀 Pronto para a próxima aula?
Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!