O que voce vai aprender neste tutorial
- Configuracao do Jest e Supertest
- Testes de endpoints GET
- Testes de endpoints POST
- Testes de APIs que requerem autenticacao
- Mocks de banco de dados
- Melhores praticas de testes
Pre-requisitos: Node.js 18 ou superior, npm/yarn/pnpm instalados. Conhecimento basico de Express facilitara o entendimento.
O que e teste de software? Por que e necessario?
Historia dos testes
O conceito de teste de software remonta aos anos 1950, mas o Desenvolvimento Orientado a Testes (TDD) moderno foi sistematizado por Kent Beck no final dos anos 1990.
“Testes nao medem qualidade. Testes constroem qualidade.” — Kent Beck
Por que escrever testes
- Prevencao de regressao: Detectar precocemente quebras em funcionalidades existentes
- Documentacao: Testes mostram como usar o codigo
- Confianca para refatoracao: Com testes, pode-se melhorar o codigo com seguranca
- Melhoria do design: Codigo testavel tende a ter um bom design
Piramide de testes
A “Piramide de Testes” proposta por Martin Fowler e um modelo que mostra os tipos e o equilibrio dos testes:
flowchart TB
subgraph Pyramid["Piramide de Testes"]
direction TB
E2E["Testes E2E (poucos)<br/>Automacao de navegador"]
Integration["Testes de Integracao (moderado)<br/>Testes de API, Testes de componentes"]
Unit["Testes Unitarios (muitos)<br/>Funcoes, Classes"]
end
E2E --> Integration --> Unit
| Tipo | Velocidade | Custo de Manutencao | Confiabilidade | Cobertura |
|---|---|---|---|---|
| E2E | Lento | Alto | Fragil | Ampla |
| Integracao | Moderado | Moderado | Moderado | Moderada |
| Unitario | Rapido | Baixo | Estavel | Estreita |
Referencia: Martin Fowler - Test Pyramid
Posicionamento dos testes de API
Testes de API sao classificados como “testes de integracao” e tem as seguintes vantagens:
- Nao sao afetados por mudancas na UI
- Mais rapidos e estaveis que E2E
- Testam requisicoes HTTP reais
- Permitem testar o backend sem frontend
Step 1: Configuracao do projeto
Primeiro, vamos criar uma aplicacao Express simples para testar.
Criacao do projeto
mkdir api-testing-tutorial
cd api-testing-tutorial
npm init -y
npm install express
npm install -D jest supertest @types/jest @types/supertest typescript ts-jest
package.json (secao scripts)
{
"scripts": {
"start": "node dist/index.js",
"build": "tsc",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Configuracao do Jest
jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.test.ts'
],
// Setup antes e depois dos testes
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
// Limite de cobertura (opcional)
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
Documentacao oficial: Jest Configuration
Step 2: Criacao da API a ser testada
src/app.ts
import express, { Express, Request, Response, NextFunction } from 'express';
const app: Express = express();
app.use(express.json());
// Armazenamento de dados em memoria
interface User {
id: number;
name: string;
email: string;
}
let users: User[] = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com' },
{ id: 2, name: '佐藤花子', email: 'sato@example.com' }
];
// Middleware de tratamento de erros
const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
};
// GET /api/users - Lista de usuarios
app.get('/api/users', (req: Request, res: Response) => {
res.json(users);
});
// GET /api/users/:id - Detalhes do usuario
app.get('/api/users/:id', (req: Request, res: Response) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'Usuario nao encontrado' });
}
res.json(user);
});
// POST /api/users - Criacao de usuario
app.post('/api/users', (req: Request, res: Response) => {
const { name, email } = req.body;
// Validacao
if (!name || !email) {
return res.status(400).json({ error: 'Nome e email sao obrigatorios' });
}
if (!email.includes('@')) {
return res.status(400).json({ error: 'Formato de email invalido' });
}
const newUser: User = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// PUT /api/users/:id - Atualizacao de usuario
app.put('/api/users/:id', (req: Request, res: Response) => {
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({ error: 'Usuario nao encontrado' });
}
const { name, email } = req.body;
users[userIndex] = { ...users[userIndex], name, email };
res.json(users[userIndex]);
});
// DELETE /api/users/:id - Exclusao de usuario
app.delete('/api/users/:id', (req: Request, res: Response) => {
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({ error: 'Usuario nao encontrado' });
}
users.splice(userIndex, 1);
res.status(204).send();
});
app.use(errorHandler);
// Funcao para resetar dados para testes
export const resetUsers = () => {
users = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com' },
{ id: 2, name: '佐藤花子', email: 'sato@example.com' }
];
};
export default app;
Step 3: Testes basicos de GET
src/app.test.ts
import request from 'supertest';
import app, { resetUsers } from './app';
describe('Users API', () => {
// Reset dos dados antes de cada teste
beforeEach(() => {
resetUsers();
});
// Testes de GET /api/users
describe('GET /api/users', () => {
it('should return all users', async () => {
const response = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toBeInstanceOf(Array);
expect(response.body.length).toBeGreaterThan(0);
});
it('should return users with correct properties', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);
const user = response.body[0];
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
});
});
// Testes de GET /api/users/:id
describe('GET /api/users/:id', () => {
it('should return a user by id', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body.id).toBe(1);
expect(response.body).toHaveProperty('name');
});
it('should return 404 for non-existent user', async () => {
const response = await request(app)
.get('/api/users/999')
.expect(404);
expect(response.body.error).toBe('Usuario nao encontrado');
});
it('should handle invalid id format', async () => {
const response = await request(app)
.get('/api/users/invalid')
.expect(404);
});
});
});
Executando os testes
npm test
# Saida de exemplo
# PASS src/app.test.ts
# Users API
# GET /api/users
# ✓ should return all users (25 ms)
# ✓ should return users with correct properties (8 ms)
# GET /api/users/:id
# ✓ should return a user by id (5 ms)
# ✓ should return 404 for non-existent user (4 ms)
Step 4: Testes de POST
Adicionando testes de criacao de dados.
describe('POST /api/users', () => {
it('should create a new user', async () => {
const newUser = {
name: '山田次郎',
email: 'yamada@example.com'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body).toMatchObject(newUser);
expect(response.body).toHaveProperty('id');
});
it('should return 400 when name is missing', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com' })
.expect(400);
expect(response.body.error).toContain('obrigatorios');
});
it('should return 400 when email is missing', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Usuario Teste' })
.expect(400);
expect(response.body.error).toContain('obrigatorios');
});
it('should return 400 for invalid email format', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Usuario Teste', email: 'invalid-email' })
.expect(400);
expect(response.body.error).toContain('formato');
});
});
Step 5: Testes de PUT/DELETE
describe('PUT /api/users/:id', () => {
it('should update an existing user', async () => {
const updatedData = {
name: '田中太郎 (Atualizado)',
email: 'tanaka-updated@example.com'
};
const response = await request(app)
.put('/api/users/1')
.send(updatedData)
.expect(200);
expect(response.body.name).toBe(updatedData.name);
expect(response.body.email).toBe(updatedData.email);
});
it('should return 404 for non-existent user', async () => {
await request(app)
.put('/api/users/999')
.send({ name: 'test', email: 'test@example.com' })
.expect(404);
});
});
describe('DELETE /api/users/:id', () => {
it('should delete an existing user', async () => {
await request(app)
.delete('/api/users/1')
.expect(204);
// Confirmar que foi excluido
await request(app)
.get('/api/users/1')
.expect(404);
});
it('should return 404 for non-existent user', async () => {
await request(app)
.delete('/api/users/999')
.expect(404);
});
});
Step 6: Padroes de teste e melhores praticas
Padrao AAA (Arrange-Act-Assert)
Estruture os testes em tres fases:
it('should create a new user', async () => {
// Arrange: Preparar dados de teste
const newUser = {
name: '山田次郎',
email: 'yamada@example.com'
};
// Act: Executar o que esta sendo testado
const response = await request(app)
.post('/api/users')
.send(newUser);
// Assert: Verificar os resultados
expect(response.status).toBe(201);
expect(response.body).toMatchObject(newUser);
});
Manter independencia dos testes
describe('Users API', () => {
// Reset dos dados antes de cada teste
beforeEach(() => {
resetUsers();
});
// Limpeza apos cada teste (se necessario)
afterEach(() => {
// Reset de mocks, etc.
jest.clearAllMocks();
});
// Limpeza apos todos os testes
afterAll(async () => {
// Fechar conexao com DB, etc.
});
});
Nomes de teste descritivos
// Bom exemplo: Especifico e intencao clara
it('should return 404 when user does not exist', () => {});
it('should create user and return 201 status', () => {});
it('should validate email format and reject invalid emails', () => {});
// Mau exemplo: Intencao nao clara
it('test1', () => {});
it('works', () => {});
it('success', () => {});
Convencao de nomenclatura: O formato “should + comportamento esperado” e comum.
Testes de valores limite
describe('Validation', () => {
it('should accept name with 1 character (minimum)', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'A', email: 'test@example.com' })
.expect(201);
});
it('should accept name with 100 characters (maximum)', async () => {
const longName = 'A'.repeat(100);
const response = await request(app)
.post('/api/users')
.send({ name: longName, email: 'test@example.com' })
.expect(201);
});
it('should reject empty name', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: '', email: 'test@example.com' })
.expect(400);
});
});
Step 7: Testes de API com autenticacao
Exemplo de testes de API com autenticacao JWT:
describe('Protected API', () => {
const validToken = 'Bearer valid-jwt-token';
const invalidToken = 'Bearer invalid-token';
it('should return 401 without authorization header', async () => {
await request(app)
.get('/api/protected/resource')
.expect(401);
});
it('should return 401 with invalid token', async () => {
await request(app)
.get('/api/protected/resource')
.set('Authorization', invalidToken)
.expect(401);
});
it('should return 200 with valid token', async () => {
await request(app)
.get('/api/protected/resource')
.set('Authorization', validToken)
.expect(200);
});
});
Step 8: Uso de mocks
Criando mocks para dependencias externas (banco de dados, APIs externas).
Mock de banco de dados
// src/services/userService.ts
import { db } from '../db';
export const userService = {
findAll: async () => db.users.findMany(),
findById: async (id: number) => db.users.findUnique({ where: { id } }),
create: async (data: { name: string; email: string }) => db.users.create({ data })
};
// src/services/userService.test.ts
import { userService } from './userService';
import { db } from '../db';
// Mock do modulo db
jest.mock('../db', () => ({
db: {
users: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn()
}
}
}));
describe('UserService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return all users', async () => {
const mockUsers = [
{ id: 1, name: 'User 1', email: 'user1@example.com' }
];
(db.users.findMany as jest.Mock).mockResolvedValue(mockUsers);
const result = await userService.findAll();
expect(db.users.findMany).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockUsers);
});
});
Mock de API externa
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('External API integration', () => {
it('should fetch data from external API', async () => {
const mockData = { data: { id: 1, title: 'Test' } };
mockedAxios.get.mockResolvedValue(mockData);
const result = await fetchExternalData();
expect(mockedAxios.get).toHaveBeenCalledWith('https://api.example.com/data');
expect(result).toEqual(mockData.data);
});
it('should handle API errors', async () => {
mockedAxios.get.mockRejectedValue(new Error('Network Error'));
await expect(fetchExternalData()).rejects.toThrow('Network Error');
});
});
Cobertura de testes
Gerando relatorio de cobertura
npm test -- --coverage
---------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
---------------------|---------|----------|---------|---------|
All files | 95.24 | 88.89 | 100 | 95.24 |
app.ts | 95.24 | 88.89 | 100 | 95.24 |
---------------------|---------|----------|---------|---------|
Tipos de cobertura
| Tipo | Descricao |
|---|---|
| Statements | Taxa de execucao de declaracoes |
| Branches | Taxa de cobertura de ramificacoes condicionais |
| Functions | Taxa de chamadas de funcoes |
| Lines | Taxa de execucao de linhas |
Nota: 100% de cobertura nao e o objetivo. Priorize testar a logica de negocios importante.
Erros comuns e antipadroes
1. Dependencia entre testes
// Mau exemplo: Depende da ordem dos testes
it('should create user', async () => {
await request(app).post('/api/users').send({ name: 'Test', email: 'test@example.com' });
});
it('should have 3 users', async () => {
// Assume que o teste acima foi executado
const response = await request(app).get('/api/users');
expect(response.body.length).toBe(3);
});
// Bom exemplo: Cada teste e independente
beforeEach(() => {
resetUsers(); // Reset dos dados
});
it('should create user', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Test', email: 'test@example.com' });
expect(response.status).toBe(201);
});
2. Testar detalhes de implementacao
// Mau exemplo: Testa implementacao interna
it('should call database with correct SQL', async () => {
await userService.findById(1);
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users WHERE id = 1');
});
// Bom exemplo: Testa o comportamento
it('should return user by id', async () => {
const user = await userService.findById(1);
expect(user.id).toBe(1);
});
3. Excesso de mocks
// Mau exemplo: Mock de tudo (teste sem sentido)
jest.mock('./userService');
jest.mock('./database');
jest.mock('./validator');
// Bom exemplo: Mock apenas de dependencias externas
jest.mock('./externalApiClient');
Resumo
Ao escrever testes de API, voce obtem os seguintes beneficios:
- Prevencao de regressao (quebra de funcionalidades existentes)
- Documentacao da especificacao da API em codigo
- Confianca ao refatorar
- Verificacao automatica no pipeline CI/CD
Comece com testes simples de GET/POST e gradualmente expanda a cobertura.
Links de referencia
Documentacao oficial
- Documentacao oficial do Jest - Framework de testes
- Supertest GitHub - Biblioteca de assercoes HTTP
- Testing Library - Melhores praticas de testes
Melhores praticas e artigos
- Martin Fowler - Test Pyramid - Explicacao da piramide de testes
- Google Testing Blog - Blog sobre testes do Google
- Kent C. Dodds - Testing JavaScript - Guia completo de testes JavaScript
Livros
- “Test Driven Development” (Kent Beck) - A obra original sobre TDD
- “Refactoring” (Martin Fowler) - Relacao entre testes e refatoracao
Ferramentas
- Jest - Framework de testes JavaScript
- Vitest - Framework de testes rapido para Vite
- Postman - Ferramenta de desenvolvimento e teste de API
- Insomnia - Cliente REST/GraphQL