Estratégias de Teste de API: De Unitário a Contrato
A qualidade de API começa com estratégia de teste. Aprenda como testes unitários, testes de integração, testes de contrato e testes E2E trabalham juntos para capturar bugs precocemente e prevenir falhas em produção.
Resumo executivo
A qualidade de API começa com estratégia de teste. Aprenda como testes unitários, testes de integração, testes de contrato e testes E2E trabalham juntos para capturar bugs precocemente e prevenir falhas em produção.
Ultima atualizacao: 11/03/2026
O problema de teste
APIs falham em produção por razões previsíveis: mudanças quebrantes escorrem, problemas de integração emergem apenas em ambientes reais e incompatibilidades de contrato entre serviços causam falhas em cascata. Abordagens de teste tradicionais que dependem pesadamente de testes E2E são lentas, frágeis e fornecem feedback tardio.
O desafio fundamental é construir confiança de que sua API funciona corretamente sem passar dias executando testes. Você precisa de feedback rápido para desenvolvedores, confiança de que serviços integram corretamente e garantia de que deployments de produção não vão quebrar consumidores downstream.
Uma abordagem estratégica de teste combina múltiplos tipos de teste em uma pirâmide: testes unitários rápidos na base, testes de integração direcionados no meio, e menos mas críticos testes E2E no topo. Teste de contrato preenche a lacuna entre serviços, capturando problemas de integração antes do deployment.
A pirâmide de teste para APIs
Entender a distribuição ótima de tipos de teste previne anti-padrões comuns como arquiteturas de "cone de sorvete" com muitos testes E2E lentos.
Testes E2E
/ \
/ \
/ \
/ Testes de Integração \
/ \
/ Testes de Contrato \
/_____________________________________\
Testes UnitáriosTestes unitários: 70% dos testes
Foco: Funções individuais, classes e módulos em isolamento.
Características:
- Rápidos (milissegundos)
- Determinísticos (sem dependências externas)
- Dependências mockadas (bancos de dados, APIs externas)
- Executam em cada commit e PR
typescript// Teste unitário: handler de endpoint API em isolamento
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { request } from 'express';
import { UsuarioController } from './UsuarioController';
import { UsuarioService } from './UsuarioService';
describe('UsuarioController.criarUsuario', () => {
let controller: UsuarioController;
let mockUsuarioService: any;
beforeEach(() => {
// Mock da dependência de serviço
mockUsuarioService = {
create: vi.fn(),
findByEmail: vi.fn()
};
controller = new UsuarioController(mockUsuarioService);
});
it('deve criar usuário com dados válidos', async () => {
const mockUsuario = { id: 1, email: 'test@example.com', nome: 'Test User' };
mockUsuarioService.create.mockResolvedValue(mockUsuario);
mockUsuarioService.findByEmail.mockResolvedValue(null);
const req = {
body: { email: 'test@example.com', nome: 'Test User' }
} as any;
const res = {
status: vi.fn().mockReturnThis(),
json: vi.fn()
} as any;
await controller.criarUsuario(req, res);
expect(mockUsuarioService.create).toHaveBeenCalledWith({
email: 'test@example.com',
nome: 'Test User'
});
expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalledWith({
data: mockUsuario
});
});
it('deve retornar 400 para formato de email inválido', async () => {
const req = {
body: { email: 'invalid-email', nome: 'Test User' }
} as any;
const res = {
status: vi.fn().mockReturnThis(),
json: vi.fn()
} as any;
await controller.criarUsuario(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.stringContaining('email')
})
);
});
it('deve retornar 409 para email duplicado', async () => {
const usuarioExistente = { id: 1, email: 'test@example.com' };
mockUsuarioService.findByEmail.mockResolvedValue(usuarioExistente);
const req = {
body: { email: 'test@example.com', nome: 'Test User' }
} as any;
const res = {
status: vi.fn().mockReturnThis(),
json: vi.fn()
} as any;
await controller.criarUsuario(req, res);
expect(res.status).toHaveBeenCalledWith(409);
expect(res.json).toHaveBeenCalledWith({
error: 'Email já existe'
});
});
});Testes de integração: 20% dos testes
Foco: Interação entre componentes e dependências externas (banco de dados, cache, fila de mensagens).
Características:
- Mais lentos (segundos a minutos)
- Dependências externas reais (banco de dados de teste)
- Múltiplos componentes trabalhando juntos
- Executam no merge de PR e pré-produção
typescript// Teste de integração: API com banco de dados real
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { setupDatabase, teardownDatabase } from './test-helpers';
import { createTestApp } from './test-app';
import request from 'supertest';
describe('POST /api/usuarios (integração)', () => {
let app: Express;
let db: Database;
beforeAll(async () => {
// Configurar banco de dados de teste com schema
db = await setupDatabase();
app = createTestApp({ database: db });
});
afterAll(async () => {
await teardownDatabase(db);
});
it('deve criar usuário e persistir no banco de dados', async () => {
const response = await request(app)
.post('/api/usuarios')
.send({
email: 'integracao@example.com',
nome: 'Integration Test User'
})
.expect(201);
expect(response.body).toMatchObject({
data: {
id: expect.any(Number),
email: 'integracao@example.com',
nome: 'Integration Test User',
criadoEm: expect.any(String)
}
});
// Verificar persistência no banco de dados
const usuario = await db.query('SELECT * FROM usuarios WHERE email = $1', ['integracao@example.com']);
expect(usuario).not.toBeNull();
expect(usuario.nome).toBe('Integration Test User');
});
it('deve lidar corretamente com requisições concorrentes', async () => {
const requests = Array(10).fill(null).map((_, i) =>
request(app)
.post('/api/usuarios')
.send({
email: `concorrente${i}@example.com`,
nome: `User ${i}`
})
);
const responses = await Promise.all(requests);
// Todas as requisições devem ter sucesso
responses.forEach((response, i) => {
expect(response.status).toBe(201);
expect(response.body.data.email).toBe(`concorrente${i}@example.com`);
});
// Verificar que todos os usuários existem no banco de dados
const count = await db.query('SELECT COUNT(*) FROM usuarios WHERE email LIKE $1', ['concorrente%']);
expect(parseInt(count.count)).toBe(10);
});
});Testes E2E: 10% dos testes
Foco: Fluxos completos de usuário através de todo o sistema.
Características:
- Mais lentos (minutos)
- Sistema real (ou ambiente de staging)
- Jornadas críticas de usuário
- Executam antes do deployment de produção
typescript// Teste E2E: fluxo completo de usuário
import { test, expect } from '@playwright/test';
test.describe('Fluxo de registro e login de usuário', () => {
test('registro e login completo de usuário', async ({ page }) => {
await page.goto('https://staging.example.com');
// Navegar para registro
await page.click('text=Registrar');
await expect(page).toHaveURL(/.*registrar/);
// Preencher formulário de registro
await page.fill('input[name="email"]', 'e2e@example.com');
await page.fill('input[name="nome"]', 'E2E Test User');
await page.fill('input[name="senha"]', 'SenhaSegura123!');
await page.fill('input[name="confirmarSenha"]', 'SenhaSegura123!');
// Enviar formulário
await page.click('button[type="submit"]');
// Verificar sucesso
await expect(page.locator('.mensagem-sucesso')).toBeVisible();
await expect(page.locator('text=Bem-vindo, E2E Test User')).toBeVisible();
// Logout
await page.click('text=Sair');
// Login
await page.fill('input[name="email"]', 'e2e@example.com');
await page.fill('input[name="senha"]', 'SenhaSegura123!');
await page.click('button[type="submit"]');
// Verificar estado logado
await expect(page.locator('text=Bem-vindo, E2E Test User')).toBeVisible();
await expect(page).toHaveURL(/.*dashboard/);
});
test('lidar com tentativas de login inválidas', async ({ page }) => {
await page.goto('https://staging.example.com/login');
// Tentar login com credenciais inválidas
await page.fill('input[name="email"]', 'inexistente@example.com');
await page.fill('input[name="senha"]', 'SenhaErrada123!');
await page.click('button[type="submit"]');
// Verificar mensagem de erro
await expect(page.locator('.mensagem-erro')).toContainText('Credenciais inválidas');
});
});Teste de contrato
Teste de contrato verifica que serviços concordam em sua interface de interação sem executar ambos serviços simultaneamente. Isso captura mudanças quebrantes antes do deployment.
Teste de contrato com Pact
typescript// Teste consumidor: Definir contrato esperado
import { Pact } from '@pact-foundation/pact';
import { like } from '@pact-foundation/dsl';
describe('Contrato de Consumidor da API de Usuário', () => {
const provider = new Pact({
consumer: 'ServicoPedido',
provider: 'ServicoUsuario',
port: 1234
});
beforeAll(async () => await provider.setup());
afterAll(async () => await provider.finalize());
afterEach(async () => await provider.verify());
it('deve buscar usuário por ID', async () => {
await provider.addInteraction({
state: 'usuário com ID 1 existe',
uponReceiving: 'uma requisição de usuário por ID',
withRequest: {
method: 'GET',
path: '/api/usuarios/1',
headers: { Accept: 'application/json' }
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
data: {
id: like(1),
email: like('test@example.com'),
nome: like('Test User'),
criadoEm: like('2024-01-01T00:00:00.000Z')
}
}
}
});
// Fazer chamada real para servidor mock do Pact
const response = await fetch('http://localhost:1234/api/usuarios/1', {
headers: { Accept: 'application/json' }
});
const data = await response.json();
expect(response.status).toBe(200);
expect(data.data.id).toBe(1);
});
});Verificação do provider
typescript// Teste provider: Verificar que serviço atende ao contrato
import { Verifier } from '@pact-foundation/pact';
import { createApp } from './app';
describe('Contrato de Provider da API de Usuário', () => {
let app: Express;
beforeAll(async () => {
app = await createApp({ database: testDatabase });
app.listen(3001);
});
it('deve satisfazer contrato do consumidor', async () => {
const verifier = new Verifier({
provider: 'ServicoUsuario',
providerBaseUrl: 'http://localhost:3001',
pactUrls: ['./pacts/ServicoPedido-ServicoUsuario.json'],
providerStatesSetupUrl: 'http://localhost:3001/api/_pact/provider-states'
});
const result = await verifier.verify();
expect(result).toEqual({
ok: true,
version: '1.0.0'
});
});
});Validação de schema OpenAPI
Usar especificações OpenAPI fornece tanto documentação quanto validação automatizada de contrato.
typescript// Teste: Validar respostas da API contra schema OpenAPI
import { OpenAPIV3 } from 'openapi-types';
import { validateResponse } from 'openapi-response-validator';
describe('Validação de contrato OpenAPI', () => {
const apiSpec: OpenAPIV3.Document = loadOpenAPISpec('./openapi.yaml');
const validator = validateResponse(apiSpec);
describe('GET /api/usuarios/:id', () => {
it('deve retornar resposta 200 válida', async () => {
const response = await request(app)
.get('/api/usuarios/1')
.expect(200);
const validation = validator('/api/usuarios/{id}', 'get', 200, response.body);
expect(validation.valid).toBe(true);
});
it('deve retornar resposta 404 válida', async () => {
const response = await request(app)
.get('/api/usuarios/999999')
.expect(404);
const validation = validator('/api/usuarios/{id}', 'get', 404, response.body);
expect(validation.valid).toBe(true);
});
});
});Teste de performance
APIs devem lidar com carga esperada sem degradação. Testes de performance capturam problemas antes da produção.
typescript// Teste de carga com k6
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up para 100 usuários
{ duration: '5m', target: 100 }, // Manter em 100 usuários
{ duration: '2m', target: 200 }, // Ramp up para 200 usuários
{ duration: '5m', target: 200 }, // Manter em 200 usuários
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% das requisições < 500ms
http_req_failed: ['rate<0.01'], // Taxa de erro < 1%
},
};
export default function () {
// Criar usuário
const payload = JSON.stringify({
email: `usuario${__VU}@example.com`,
nome: `User ${__VU}`
});
const params = {
headers: { 'Content-Type': 'application/json' },
};
const res = http.post('https://api.example.com/usuarios', payload, params);
check(res, {
'status foi 201': (r) => r.status === 201,
'tempo de resposta < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}Teste de segurança
Testes de segurança verificam que APIs protegem contra vulnerabilidades comuns.
typescript// Exemplos de teste de segurança
import { test, expect } from '@playwright/test';
test.describe('Testes de Segurança de API', () => {
test('deve rejeitar tentativas de SQL injection', async ({ request }) => {
const response = await request.post('https://api.example.com/usuarios', {
data: {
email: "'; DROP TABLE usuarios; --",
nome: 'Tentativa de Ataque'
}
});
expect(response.status()).toBe(400);
expect(await response.json()).toMatchObject({
error: expect.stringContaining('validacao')
});
});
test('deve requerer autenticação para endpoints protegidos', async ({ request }) => {
const response = await request.get('https://api.example.com/api/admin/usuarios');
expect(response.status()).toBe(401);
});
test('deve rejeitar payloads sobrecarregados', async ({ request }) => {
const largePayload = 'x'.repeat(10 * 1024 * 1024); // 10MB
const response = await request.post('https://api.example.com/usuarios', {
data: { email: largePayload, nome: 'Test' }
});
expect(response.status()).toBe(413);
});
test('deve impor limitação de taxa', async ({ request }) => {
const requests = Array(100).fill(null).map(() =>
request.get('https://api.example.com/api/usuarios')
);
const responses = await Promise.all(requests);
const rateLimitedResponses = responses.filter(r => r.status() === 429);
expect(rateLimitedResponses.length).toBeGreaterThan(0);
});
});Organização e estrutura de testes
Organizar testes efetivamente mantém clareza e reduz duplicação.
typescript// Helpers e fixtures de teste
import { TestFactory } from './TestFactory';
describe('API de Usuário', () => {
const factory = new TestFactory();
beforeAll(async () => {
await factory.setup();
});
afterAll(async () => {
await factory.teardown();
});
describe('POST /api/usuarios', () => {
it('deve criar usuário', async () => {
const dadosUsuario = factory.dadosUsuario();
const response = await factory.request()
.post('/api/usuarios')
.send(dadosUsuario)
.expect(201);
expect(response.body.data).toMatchObject({
id: expect.any(Number),
...dadosUsuario
});
});
});
describe('GET /api/usuarios/:id', () => {
it('deve retornar usuário', async () => {
const usuario = await factory.criarUsuario();
const response = await factory.request()
.get(`/api/usuarios/${usuario.id}`)
.expect(200);
expect(response.body.data).toMatchObject({
id: usuario.id,
email: usuario.email
});
});
});
});Anti-padrões comuns de teste
Anti-padrão 1: Testar detalhes de implementação
typescript// RUIM: Testar implementação
it('deve chamar database.query', () => {
expect(mockDatabase.query).toHaveBeenCalledWith(
'SELECT * FROM usuarios WHERE id = $1',
[1]
);
});
// BOM: Testar comportamento
it('deve retornar usuário quando ID existe', () => {
const usuario = await controller.getUsuario(1);
expect(usuario).toMatchObject({
id: 1,
email: expect.any(String)
});
});Anti-padrão 2: Over-mocking em testes de integração
typescript// RUIM: Mockar banco de dados em teste de integração
beforeEach(() => {
mockDatabase.query.mockResolvedValue(testUser);
});
// BOM: Usar banco de dados de teste em teste de integração
beforeAll(async () => {
db = await setupTestDatabase();
});Anti-padrão 3: Testes E2E frágeis
typescript// RUIM: Selecionar por detalhes de implementação
await page.click('.btn-primary:nth-child(2)');
// BOM: Selecionar por atributos voltados ao usuário
await page.click('button[type="submit"]');
await page.click('text=Criar Usuário');Pipeline de automação de teste
Integrar testes em CI/CD garante gates de qualidade.
yaml# Workflow do GitHub Actions
name: Testes de API
on: [push, pull_request]
jobs:
testes-unitarios:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npm run test:unit -- --coverage
testes-integracao:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run test:integration
testes-contrato:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run test:contract
testes-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run test:e2e
testes-performance:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run test:performanceConclusão
Estratégia de teste de API combina múltiplos tipos de teste em uma pirâmide: testes unitários rápidos para feedback rápido, testes de integração para interação de componentes, testes de contrato para compatibilidade de serviço, e testes E2E seletivos para workflows críticos.
A chave é equilíbrio. Muitos testes E2E criam suites de teste lentas e frágeis. Poucos testes de integração perdem bugs que apenas aparecem com dependências reais. Teste de contrato preenche a lacuna entre serviços sem requerer que ambos executem simultaneamente.
Comece com testes unitários que focam em comportamento, não implementação. Adicione testes de integração para caminhos críticos com dependências reais. Implemente teste de contrato para fronteiras de serviço. Use testes E2E moderadamente para jornadas completas de usuário. Execute testes em CI/CD para capturar problemas cedo, e itere na eficácia dos testes baseado em incidentes de produção.
Sua estratégia de teste de API precisa de refinamento para prevenir falhas de produção? Fale com especialistas em engenharia da Imperialis sobre estratégias abrangentes de teste de API, implementação de teste de contrato e integração CI/CD para seu ciclo de vida de desenvolvimento de API.
Fontes
- Testing Library: Best Practices — melhores práticas de teste
- Pact: Consumer-Driven Contract Testing — documentação de teste de contrato
- OpenAPI Specification — padrão de especificação de API
- k6: Load Testing Tool — documentação de teste de performance