Node.js: Testes e Depuração
Última atualização: 2026-08-26
Bob escreveu uma API RESTful para uma plataforma de e-commerce e, toda vez que adicionava um novo recurso, algo quebrava. Testar manualmente com o Postman levava 30 minutos por implantação, e ele ainda deixava passar casos extremos. Ele introduziu o Jest para testes unitários e o Supertest para testes de integração de API — agora o npm test executa 50 testes em 3 segundos, e cada implantação é respaldada por uma verificação de CI automatizada. Ele também adicionou o winston para logging estruturado, de modo que, quando um bug escapa, os logs dizem exatamente o que deu errado.
Você aprenderá:
- Testes unitários com Jest (describe / it / expect / mock)
- Testes de integração de API com Supertest (simulação de requisições HTTP)
- Depurador nativo do Node.js (node inspect / Chrome DevTools)
- Logging estruturado com winston (transports / format / rotation)
- Cobertura de testes e fluxo de trabalho TDD
1. Visão geral dos testes
(1) Por que escrever testes
| Sem testes | Com testes |
|---|---|
| Verificação manual, 30 min a cada execução | Automatizado, 3 segundos |
| Alterar uma coisa, quebrar tudo | Alterar uma coisa, os testes indicam o que quebrou |
| Ansiedade antes da implantação | Sinal verde, implantar com confiança |
| Medo de refatorar | Refatorar com a rede de segurança dos testes |
▶ Exemplo: (2) Pirâmide de Testes
graph TD
A["Unit Tests<br/>Many / Fast / Cheap"] --> B["Integration Tests<br/>Moderate / Slower"]
B --> C["E2E Tests<br/>Few / Slow / Expensive"]
style A fill:#4CAF50,color:#fff
style B fill:#FF9800,color:#fff
style C fill:#F44336,color:#fff
- Testes unitários: testam funções/módulos individuais, rápidos, em maior quantidade
- Testes de integração: testam a cooperação entre módulos (ex.: API + banco de dados), velocidade moderada
- Testes E2E: simulam operações do usuário, mais lentos, em menor quantidade
(3) Comparação de ferramentas de teste do Node.js
| Ferramenta | Tipo | Recursos | Ideal para |
|---|---|---|---|
| Jest | Unitário + Integração | Configuração zero, mock/cobertura integrados, teste de snapshot | Uso geral |
| Mocha | Unitário + Integração | Flexível, muitos plugins, requer chai/sinon | Necessidades personalizadas |
| Vitest | Unitário | Ecossistema Vite, ESM nativo, muito rápido | Projetos Vite |
| node:test | Unitário | Nativo do Node.js, sem dependências | Projetos simples |
| Supertest | Teste HTTP | Simulação de requisições HTTP, teste de rotas Express | Testes de integração de API |
| Playwright | E2E | Automação de navegador, suporte a vários navegadores | E2E de front-end |
Esta lição usa a combinação Jest + Supertest, a solução de testes mais difundida na comunidade Node.js.
2. Testes unitários com Jest
▶ Exemplo: (1) Instalação e configuração
npm install --save-dev jest
Adicione o script de teste ao package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
O Jest funciona sem configuração por padrão, localizando automaticamente os arquivos *.test.js ou *.spec.js.
▶ Exemplo: (2) Sintaxe básica: describe / it / expect
// math.js — o módulo a ser testado
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
module.exports = { add, subtract, multiply, divide };
// math.test.js
const { add, subtract, multiply, divide } = require('./math');
describe('Math utilities', () => {
test('add should sum two numbers', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
});
test('subtract should return the difference', () => {
expect(subtract(5, 3)).toBe(2);
});
test('multiply should return the product', () => {
expect(multiply(3, 4)).toBe(12);
});
test('divide should throw on zero divisor', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Executar:
npm test
Output:
PASS ./math.test.js
Math utilities
✓ add should sum two numbers (2 ms)
✓ subtract should return the difference
✓ multiply should return the product
✓ divide should throw on zero divisor (1 ms)
Tests: 4 passed, 4 total
Time: 0.5s
(3) Matchers comuns
| Matcher | Significado | Exemplo |
|---|---|---|
toBe |
Igualdade estrita (===) | expect(1 + 1).toBe(2) |
toEqual |
Igualdade profunda | expect(obj).toEqual({ a: 1 }) |
toBeTruthy / toBeFalsy |
Verificação booleana | expect(flag).toBeTruthy() |
toBeNull / toBeUndefined |
null / undefined | expect(val).toBeNull() |
toThrow |
Lança exceção | expect(fn).toThrow() |
toContain |
O array contém | expect([1, 2, 3]).toContain(2) |
toMatch |
Correspondência por regex | expect('hello').toMatch(/ell/) |
toHaveLength |
Verificação de comprimento | expect('abc').toHaveLength(3) |
resolves / rejects |
Resultado de Promise | expect(promise).resolves.toBe(5) |
toHaveBeenCalled |
Mock chamado | expect(fn).toHaveBeenCalled() |
▶ Exemplo: (4) Testes assíncronos
// async-functions.js
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: `User_${id}` });
}, 100);
});
}
async function getUserEmail(id) {
const user = await fetchUser(id);
return `${user.name.toLowerCase()}@example.com`;
}
module.exports = { fetchUser, getUserEmail };
// async-functions.test.js
const { fetchUser, getUserEmail } = require('./async-functions');
describe('Async function tests', () => {
test('fetchUser returns user object', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'User_1' });
});
test('getUserEmail returns formatted email', async () => {
const email = await getUserEmail(42);
expect(email).toBe('user_42@example.com');
});
test('fetchUser resolves within 200ms', async () => {
await expect(fetchUser(1)).resolves.toHaveProperty('id', 1);
});
});
(5) Mocks (simulação)
Os mocks isolam dependências externas (bancos de dados, requisições HTTP, sistema de arquivos) para que os testes se concentrem apenas na lógica do módulo atual.
// email-service.js
class EmailService {
send(to, subject, body) {
// chamada SMTP real — lenta demais para testes unitários
console.log(`Sending email to ${to}: ${subject}`);
return true;
}
}
class UserService {
constructor(emailService) {
this.emailService = emailService;
}
register(name, email) {
// lógica de negócio: validar + enviar e-mail de boas-vindas
if (!email.includes('@')) throw new Error('Invalid email');
this.emailService.send(email, 'Welcome', `Hello ${name}!`);
return { name, email, status: 'registered' };
}
}
module.exports = { EmailService, UserService };
// email-service.test.js
const { EmailService, UserService } = require('./email-service');
describe('UserService', () => {
let mockEmailService;
let userService;
beforeEach(() => {
mockEmailService = {
send: jest.fn().mockReturnValue(true),
};
userService = new UserService(mockEmailService);
});
test('register should call emailService.send', () => {
const result = userService.register('Alice', 'alice@example.com');
expect(result).toEqual({
name: 'Alice',
email: 'alice@example.com',
status: 'registered',
});
expect(mockEmailService.send).toHaveBeenCalledWith(
'alice@example.com',
'Welcome',
'Hello Alice!'
);
});
test('register should throw on invalid email', () => {
expect(() => userService.register('Bob', 'invalid')).toThrow('Invalid email');
expect(mockEmailService.send).not.toHaveBeenCalled();
});
test('register should send exactly one email', () => {
userService.register('Charlie', 'charlie@example.com');
expect(mockEmailService.send).toHaveBeenCalledTimes(1);
});
});
▶ Exemplo: Asserções básicas com Jest
test('basic matchers', () => {
expect(2 + 2).toBe(4);
expect({ name: 'Alice' }).toEqual({ name: 'Alice' });
expect([1, 2, 3]).toContain(2);
expect('hello world').toMatch(/world/);
expect(() => { throw new Error('fail'); }).toThrow('fail');
});
Mostra os matchers do Jest mais usados: toBe para primitivos, toEqual para objetos, toContain para arrays, toMatch para strings e toThrow para testes de erro.
3. Testes de integração de API com Supertest
▶ Exemplo: (1) Instalação e princípio
npm install --save-dev supertest
O Supertest inicia uma aplicação Express em memória, envia requisições HTTP e faz asserções sobre as respostas — sem a necessidade de escutar em uma porta real.
flowchart LR
A["Test File"] -->|"request(app)"| B["Express App<br/>(in-memory)"]
B -->|"response"| A
A -->|"assert"| C["Jest expect"]
▶ Exemplo: (2) Testando a API Express
// app.js — a aplicação Express (exportada sem listen)
const express = require('express');
const app = express();
app.use(express.json());
let products = [
{ id: 1, name: 'Laptop', price: 999.99 },
{ id: 2, name: 'Phone', price: 499.99 },
];
app.get('/api/products', (req, res) => {
res.json(products);
});
app.get('/api/products/:id', (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) return res.status(404).json({ error: 'Product not found' });
res.json(product);
});
app.post('/api/products', (req, res) => {
const { name, price } = req.body;
if (!name || price == null) {
return res.status(400).json({ error: 'Name and price are required' });
}
const newProduct = { id: products.length + 1, name, price };
products.push(newProduct);
res.status(201).json(newProduct);
});
module.exports = app;
Observação: o
app.jsapenas exporta oapp; ele não chamaapp.listen()dentro do módulo. Olistenfica noserver.js, para que o Supertest possa testar o objetoappdiretamente.
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('Products API', () => {
test('GET /api/products should return product list', async () => {
const response = await request(app).get('/api/products');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
expect(response.body[0]).toHaveProperty('name', 'Laptop');
});
test('GET /api/products/:id should return a single product', async () => {
const response = await request(app).get('/api/products/1');
expect(response.status).toBe(200);
expect(response.body.id).toBe(1);
});
test('GET /api/products/:id should return 404 for missing product', async () => {
const response = await request(app).get('/api/products/999');
expect(response.status).toBe(404);
expect(response.body).toHaveProperty('error', 'Product not found');
});
test('POST /api/products should create a new product', async () => {
const response = await request(app)
.post('/api/products')
.send({ name: 'Tablet', price: 299.99 });
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('name', 'Tablet');
expect(response.body).toHaveProperty('id', 3);
});
test('POST /api/products should return 400 for missing fields', async () => {
const response = await request(app)
.post('/api/products')
.send({ name: 'Incomplete Product' });
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error');
});
});
▶ Exemplo: (3) Testando API autenticada
// protected-routes.test.js
const request = require('supertest');
const app = require('./app');
describe('Protected API routes', () => {
let token;
beforeAll(async () => {
// login para obter o token JWT
const response = await request(app)
.post('/api/auth/login')
.send({ username: 'alice', password: 'secret123' });
token = response.body.token;
});
test('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('username', 'alice');
});
test('should reject access without token', async () => {
const response = await request(app).get('/api/profile');
expect(response.status).toBe(401);
});
test('should reject access with invalid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid_token_here');
expect(response.status).toBe(401);
});
});
▶ Exemplo: Teste de verificação de saúde da API
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/health', (req, res) => res.json({ status: 'ok' }));
describe('GET /health', () => {
it('returns 200 with status ok', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
});
});
Um teste mínimo de verificação de saúde da API usando o Supertest — cria uma aplicação Express básica, envia uma requisição GET e verifica o corpo da resposta JSON e o código de status.
4. Depuração no Node.js
(1) node inspect + Chrome DevTools
O Node.js tem um depurador embutido — sem necessidade de ferramentas extras.
node inspect app.js
Em seguida, abra chrome://inspect no Chrome e clique em "Open dedicated DevTools for Node".
Operações comuns de depuração:
| Ação | Atalho do DevTools | Linha de comando |
|---|---|---|
| Continuar | F8 | c |
| Passar por cima | F10 | n |
| Entrar | F11 | s |
| Sair | Shift+F11 | o |
| Definir breakpoint | Clicar no número da linha | setBreakpoint() |
▶ Exemplo: (2) Definindo breakpoints no código
// debug-demo.js
function calculateDiscount(price, memberLevel) {
debugger; // a execução pausa aqui no inspetor
let discount = 0;
if (memberLevel === 'gold') discount = 0.2;
else if (memberLevel === 'silver') discount = 0.1;
const finalPrice = price * (1 - discount);
return finalPrice;
}
console.log(calculateDiscount(100, 'gold'));
console.log(calculateDiscount(100, 'silver'));
console.log(calculateDiscount(100, 'unknown'));
node inspect debug-demo.js
▶ Exemplo: (3) Dicas de depuração com console
// métodos de depuração com console
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'admin' },
];
// Exibição em tabela
console.table(users);
// Medir o tempo de execução
console.time('database-query');
// ... lógica da consulta ...
console.timeEnd('database-query');
// Rastreamento de pilha
console.trace('Where is this called from?');
// Saída agrupada
console.group('User Details');
console.log('Name: Alice');
console.log('Role: admin');
console.groupEnd();
5. Logging estruturado com winston
(1) Por que usar winston em vez de console.log
| console.log | winston |
|---|---|
| Sem níveis de log | Suporta debug/info/warn/error |
| Sem saída em arquivo | Suporta Console + File + HTTP e outros transports |
| Sem formatação | Suporta JSON / timestamp / formatos personalizados |
| Sem rotação de log | Junte ao winston-daily-rotate-file para rotação automática |
| Não recomendado para produção | Escolha de nível de produção |
▶ Exemplo: (2) Instalação e configuração básica
npm install winston
npm install winston-daily-rotate-file
// logger.js
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'my-app' },
transports: [
// Logs de erro — arquivo separado
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
maxSize: '20m',
maxFiles: '14d',
}),
// Todos os logs — arquivo combinado
new DailyRotateFile({
filename: 'logs/combined-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
}),
],
});
// Desenvolvimento: também registrar no console com saída colorida
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}));
}
module.exports = logger;
▶ Exemplo: (3) Uso em uma aplicação Express
// app-with-logger.js
const express = require('express');
const logger = require('./logger');
const app = express();
app.use(express.json());
// Middleware de logging de requisições
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
});
});
next();
});
app.get('/api/health', (req, res) => {
logger.debug('Health check requested');
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id;
logger.info('User lookup', { userId });
if (isNaN(userId)) {
logger.warn('Invalid user ID format', { userId });
return res.status(400).json({ error: 'Invalid user ID' });
}
// Simular busca de usuário
const user = { id: parseInt(userId), name: 'Alice' };
logger.info('User found', { userId: user.id, name: user.name });
res.json(user);
});
app.use((err, req, res, next) => {
logger.error('Unhandled error', { error: err.message, stack: err.stack });
res.status(500).json({ error: 'Internal server error' });
});
module.exports = app;
Exemplo de saída do arquivo de log:
{"level":"info","message":"HTTP request","service":"my-app","timestamp":"2026-07-13 10:30:00","method":"GET","url":"/api/users/1","status":200,"duration":"15ms"}
{"level":"warn","message":"Invalid user ID format","service":"my-app","timestamp":"2026-07-13 10:30:05","userId":"abc"}
{"level":"error","message":"Unhandled error","service":"my-app","timestamp":"2026-07-13 10:31:00","error":"Cannot read property 'name' of undefined","stack":"TypeError: Cannot read property..."}
▶ Exemplo: Logging estruturado com winston
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
logger.info('Server started', { port: 3000, env: 'development' });
logger.warn('Memory usage high', { used: '850MB' });
logger.error('Database connection failed', { db: 'main', retry: 3 });
Demonstra o logging JSON estruturado com winston — diferentes níveis de log (info, warn, error) e metadados contextuais anexados a cada entrada de log.
6. Cobertura de testes e TDD
(1) Cobertura de testes
O Jest inclui o Istanbul para relatórios de cobertura:
npm run test:coverage
Output:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 90.5 | 85.71 | 100 | 90.5 |
math.js | 100 | 100 | 100 | 100 |
app.js | 88.24 | 83.33 | 100 | 88.24 | 45-48
----------|---------|----------|---------|---------|-------------------
Dimensões de cobertura explicadas:
| Dimensão | Significado | Meta sugerida |
|---|---|---|
| Statements | Porcentagem de instruções executadas | ≥ 80% |
| Branches | Porcentagem de ramificações condicionais executadas | ≥ 75% |
| Functions | Porcentagem de funções chamadas | ≥ 85% |
| Lines | Porcentagem de linhas de código executadas | ≥ 80% |
Buscar 100% de cobertura tem retornos decrescentes; uma cobertura de 80% detecta a maioria dos bugs.
▶ Exemplo: (2) Fluxo de trabalho TDD (Red-Green-Refactor)
flowchart LR
A["🔴 Red<br/>Write failing test"] --> B["🟢 Green<br/>Write minimal code to pass"]
B --> C["🔵 Refactor<br/>Improve code while tests pass"]
C --> A
- Red: Escreva um teste primeiro (ele falhará porque o recurso ainda não existe)
- Green: Escreva o código mínimo para fazer o teste passar
- Refactor: Melhore o código enquanto os testes continuam passando
▶ Exemplo: Funções utilitárias de string com TDD
// string-utils.test.js — escreva o teste PRIMEIRO (Red)
const { capitalize, truncate, slugify } = require('./string-utils');
describe('StringUtils', () => {
describe('capitalize', () => {
test('should capitalize first letter', () => {
expect(capitalize('hello')).toBe('Hello');
});
test('should handle empty string', () => {
expect(capitalize('')).toBe('');
});
test('should handle already capitalized', () => {
expect(capitalize('Hello')).toBe('Hello');
});
});
describe('truncate', () => {
test('should truncate long strings', () => {
expect(truncate('Hello World', 5)).toBe('Hello...');
});
test('should not truncate short strings', () => {
expect(truncate('Hi', 10)).toBe('Hi');
});
});
describe('slugify', () => {
test('should convert to URL slug', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
test('should handle special characters', () => {
expect(slugify('C# & .NET!')).toBe('c-and-net');
});
});
});
// string-utils.js — escreva a implementação DEPOIS do teste (Green)
function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
function truncate(str, maxLength) {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + '...';
}
function slugify(str) {
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
module.exports = { capitalize, truncate, slugify };
7. Exemplo abrangente: escrevendo uma suíte de testes completa para uma API de gerenciamento de tarefas
▶ Exemplo: Suíte de testes da API de tarefas
Passo 1 — Código da aplicação
// tasks-app.js
const express = require('express');
const app = express();
app.use(express.json());
let tasks = [];
let nextId = 1;
function resetTasks() {
tasks = [];
nextId = 1;
}
app.get('/api/tasks', (req, res) => {
const { status, priority } = req.query;
let filtered = tasks;
if (status) filtered = filtered.filter(t => t.status === status);
if (priority) filtered = filtered.filter(t => t.priority === priority);
res.json(filtered);
});
app.get('/api/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
app.post('/api/tasks', (req, res) => {
const { title, priority = 'medium' } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' });
const task = { id: nextId++, title, priority, status: 'pending' };
tasks.push(task);
res.status(201).json(task);
});
app.patch('/api/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
const { title, status, priority } = req.body;
if (title) task.title = title;
if (status) task.status = status;
if (priority) task.priority = priority;
res.json(task);
});
app.delete('/api/tasks/:id', (req, res) => {
const index = tasks.findIndex(t => t.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: 'Task not found' });
const deleted = tasks.splice(index, 1);
res.json(deleted[0]);
});
module.exports = { app, resetTasks };
Passo 2 — Suíte de testes completa
// tasks-app.test.js
const request = require('supertest');
const { app, resetTasks } = require('./tasks-app');
beforeEach(() => {
resetTasks();
});
describe('Tasks API — CRUD operations', () => {
test('should create a task', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ title: 'Write tests', priority: 'high' });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
id: 1,
title: 'Write tests',
priority: 'high',
status: 'pending',
});
});
test('should reject task without title', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ priority: 'high' });
expect(response.status).toBe(400);
expect(response.body.error).toBe('Title is required');
});
test('should list all tasks', async () => {
await request(app).post('/api/tasks').send({ title: 'Task 1' });
await request(app).post('/api/tasks').send({ title: 'Task 2' });
const response = await request(app).get('/api/tasks');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
});
test('should filter tasks by status', async () => {
await request(app).post('/api/tasks').send({ title: 'Pending task' });
const createRes = await request(app).post('/api/tasks').send({ title: 'Done task' });
await request(app).patch(`/api/tasks/${createRes.body.id}`).send({ status: 'done' });
const response = await request(app).get('/api/tasks?status=done');
expect(response.body).toHaveLength(1);
expect(response.body[0].status).toBe('done');
});
test('should update a task', async () => {
const createRes = await request(app).post('/api/tasks').send({ title: 'Old title' });
const response = await request(app)
.patch(`/api/tasks/${createRes.body.id}`)
.send({ title: 'New title', status: 'done' });
expect(response.status).toBe(200);
expect(response.body.title).toBe('New title');
expect(response.body.status).toBe('done');
});
test('should delete a task', async () => {
const createRes = await request(app).post('/api/tasks').send({ title: 'To delete' });
const response = await request(app).delete(`/api/tasks/${createRes.body.id}`);
expect(response.status).toBe(200);
const listRes = await request(app).get('/api/tasks');
expect(listRes.body).toHaveLength(0);
});
test('should return 404 for non-existent task', async () => {
const response = await request(app).get('/api/tasks/999');
expect(response.status).toBe(404);
});
});
Executar:
npm test
Output:
PASS ./tasks-app.test.js
Tasks API — CRUD operations
✓ should create a task (15 ms)
✓ should reject task without title (3 ms)
✓ should list all tasks (4 ms)
✓ should filter tasks by status (8 ms)
✓ should update a task (5 ms)
✓ should delete a task (5 ms)
✓ should return 404 for non-existent task (2 ms)
Tests: 7 passed, 7 total
Time: 0.8s
❓ Perguntas Frequentes
P: Devo escolher Jest ou Mocha? R: O Jest tem configuração zero com mock/cobertura integrados, ótimo para a maioria dos projetos; o Mocha é flexível, mas precisa de chai/sinon/istanbul, sendo ideal para projetos que exigem personalização profunda. O Jest é recomendado para novos projetos.
P: Qual é a diferença entre testes unitários e testes de integração? R: Os testes unitários isolam e testam funções/módulos individuais usando mocks para substituir dependências externas — são rápidos. Os testes de integração verificam a cooperação entre vários módulos (ex.: API + banco de dados) usando componentes reais — são mais lentos, mas mais próximos dos cenários reais.
P: Os testes precisam de uma conexão real com o banco de dados? R: Os testes unitários nunca devem usar um banco de dados real — use mocks. Os testes de integração podem usar um banco de dados em memória (ex.: SQLite
:memory:) ou uma instância de banco de dados de teste dedicada. Os testes E2E usam um banco de dados do ambiente de teste.
P: Quais são os níveis de log do winston? R: Do menor para o maior: error → warn → info → http → verbose → debug → silly. Quando o nível é definido como
info, apenasinfoe acima são emitidos.
P: Como depurar os próprios testes do Jest? R: Execute
node --inspect-brk node_modules/.bin/jest --runInBande depure no Chrome DevTools. O--runInBandgarante que o Jest execute em uma única thread para que os breakpoints funcionem.
P: O Supertest precisa iniciar um servidor real? R: Não. O Supertest cria um servidor em memória via
http.createServer(app)— nenhuma porta é ocupada e ele fecha automaticamente quando os testes terminam.
📖 Resumo
- Pirâmide de testes: Testes unitários (muitos, rápidos, baratos) → Testes de integração (moderados) → Testes E2E (poucos, lentos, caros)
- Testes unitários com Jest sem configuração: describe/it/expect + mocks para dependências externas
- O Supertest testa a API Express em memória: request(app).get/post/patch/delete
- Depurador nativo
node inspectdo Node.js com breakpoints visuais no Chrome DevTools - O winston oferece logging estruturado: vários níveis + vários transports + rotação de log
- Cobertura de teste de 80%+ é suficiente; o fluxo TDD Red→Green→Refactor melhora a qualidade do código
📝 Exercícios
- Escreva testes unitários com Jest para a seguinte função:
function isPalindrome(str) { return str === str.split('').reverse().join(''); }, cobrindo palíndromos normais, não palíndromos, strings vazias e cenários com letras maiúsculas e minúsculas. - Crie uma API Express (CRUD para
/api/books) e escreva pelo menos 5 testes de integração usando o Supertest (criar, ler, atualizar, excluir, 404). - Adicione um middleware de logging com winston a uma aplicação Express existente, registrando método/url/status/duração para cada requisição, com rotação diária de logs.
- Use TDD para desenvolver uma função
formatCurrency(amount, currency): primeiro escreva um teste que falhe (ex.:formatCurrency(1234.5, 'USD')→'$1,234.50') e depois escreva a implementação. - Execute
jest --coverage, encontre arquivos com cobertura abaixo de 80% e adicione casos de teste para atingir a meta de cobertura.