Testes de contrato com Pact: validando integrações entre serviços
1. Fundamentos dos Testes de Contrato
Os testes de contrato ocupam uma posição estratégica no espectro de testes de software. Diferentemente dos testes unitários, que validam unidades isoladas de código, ou dos testes de integração, que verificam a comunicação entre componentes internos, os testes de contrato focam exclusivamente nas interfaces entre serviços. Enquanto testes E2E percorrem fluxos completos e são lentos e frágeis, os testes de contrato são rápidos, determinísticos e focados na compatibilidade da comunicação.
Em arquiteturas de microsserviços, onde dezenas ou centenas de serviços se comunicam via APIs, a principal dor é garantir que mudanças em um serviço não quebrem seus consumidores. Testes de contrato resolvem isso definindo expectativas formais — os pactos — entre quem consome (consumidor) e quem fornece (provedor) um recurso. Cada interação documentada no pacto especifica: a requisição que o consumidor fará, a resposta esperada do provedor, e os estados que o provedor deve estar para responder corretamente.
2. O Pact como Ferramenta de Testes de Contrato
Pact é a ferramenta mais difundida para testes de contrato baseados em consumidor. Criado originalmente em Ruby pela DiUS (Austrália), hoje possui implementações maduras para Java, JavaScript/TypeScript, Python, Go, .NET e outras linguagens. O diferencial do Pact é sua abordagem "consumer-driven": o consumidor define o contrato primeiro, gerando um arquivo JSON (o pacto) que descreve exatamente o que espera do provedor.
A estrutura de um pacto inclui:
- Interação: par requisição-resposta
- Matchers: regras flexíveis para campos dinâmicos (datas, UUIDs, arrays)
- Provider states: condições sob as quais o provedor deve estar para responder
3. Escrevendo Testes no Lado do Consumidor
Vamos a um exemplo prático com JavaScript/TypeScript usando a biblioteca @pact-foundation/pact.
// consumer/order-service.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { OrderService } from './order-service';
const provider = new PactV3({
consumer: 'OrderService',
provider: 'StockService',
});
describe('OrderService - consulta estoque', () => {
it('deve retornar quantidade disponível para um produto', async () => {
// Define a interação esperada
provider
.given('produto com id 123 existe no estoque')
.uponReceiving('uma requisição de consulta de estoque')
.withRequest({
method: 'GET',
path: '/stock/123',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
productId: MatchersV3.string('123'),
quantity: MatchersV3.integer(10),
lastUpdated: MatchersV3.isoDate(),
},
});
// Executa o teste contra o mock do provedor
await provider.executeTest(async (mockServer) => {
const client = new OrderService(mockServer.url);
const result = await client.checkStock('123');
expect(result.quantity).toBeGreaterThanOrEqual(0);
});
});
});
Ao executar o teste, o Pact gera automaticamente um arquivo pacts/OrderService-StockService.json com o contrato. Esse arquivo deve ser versionado e compartilhado com a equipe do provedor.
4. Verificando o Contrato no Lado do Provedor
No provedor, utilizamos o Pact para verificar se o serviço real atende ao contrato definido pelo consumidor.
// provider/stock-service-verification.test.ts
import { Verifier } from '@pact-foundation/pact';
import { startServer, stopServer } from './server';
describe('Verificação do contrato StockService', () => {
let server;
beforeAll(async () => {
server = await startServer(3001);
});
afterAll(async () => {
await stopServer(server);
});
it('deve validar os pactos dos consumidores', async () => {
const verifier = new Verifier({
providerBaseUrl: 'http://localhost:3001',
pactUrls: ['./pacts/OrderService-StockService.json'],
stateHandlers: {
'produto com id 123 existe no estoque': async () => {
// Prepara o estado no banco de dados
await seedProduct({ id: '123', quantity: 10 });
},
},
});
await verifier.verifyProvider();
});
});
Os estados do provedor (provider states) são hooks que preparam dados específicos antes de cada verificação. Para provedores com múltiplos consumidores, basta adicionar mais arquivos de pacto ou apontar para um broker.
5. Integração Contínua e Ciclo de Vida dos Pactos
O Pact Broker é o componente central para gerenciar contratos em escala. Ele armazena versões de pactos, relaciona consumidores com provedores e permite visualizar dependências entre serviços.
Em um pipeline de CI típico:
# Pipeline de CI - Consumidor
1. Executa testes do consumidor → gera pacto
2. Publica pacto no broker (com versão do consumidor)
3. Se for merge na main, marca como "production-ready"
# Pipeline de CI - Provedor
1. Verifica todos os pactos publicados contra o provedor
2. Se houver falha, o deploy é bloqueado
3. Se passar, publica verificação no broker
Estratégias avançadas incluem canary releases onde apenas uma fração do tráfego vai para a nova versão, e rollback automático se o broker detectar incompatibilidade nos contratos.
6. Padrões Avançados e Desafios Práticos
Matchers flexíveis são essenciais para lidar com dados dinâmicos:
body: {
id: MatchersV3.uuid(),
createdAt: MatchersV3.isoDate(),
metadata: MatchersV3.eachLike({
key: MatchersV3.string('default'),
value: MatchersV3.string('value'),
}),
optionalField: MatchersV3.boolean(true),
}
Para provedores assíncronos (mensageria, filas), o Pact oferece suporte através de mensagens Pact (Pact for Message Queues). O consumidor define a mensagem esperada e o provedor verifica se consegue produzi-la.
Limitações importantes do Pact:
- Não substitui testes de performance ou segurança
- Para APIs públicas (muitos consumidores desconhecidos), OpenAPI/Swagger pode ser mais adequado
- gRPC tem seu próprio ecossistema de testes de contrato (protobuf)
7. Exemplo Prático: Validação de uma API REST entre Serviços
Cenário: Serviço de Pedidos (consumidor) consulta Serviço de Estoque (provedor) para verificar disponibilidade antes de criar um pedido.
Teste do consumidor (já apresentado na seção 3) gera o pacto:
{
"consumer": { "name": "OrderService" },
"provider": { "name": "StockService" },
"interactions": [{
"description": "uma requisição de consulta de estoque",
"providerState": "produto com id 123 existe no estoque",
"request": {
"method": "GET",
"path": "/stock/123",
"headers": { "Accept": "application/json" }
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"body": {
"productId": "123",
"quantity": 10,
"lastUpdated": "2024-01-15"
}
}
}],
"metadata": {
"pactSpecification": { "version": "3.0.0" }
}
}
Verificação no provedor: O serviço de estoque executa a verificação, que falha se, por exemplo, o campo lastUpdated mudar de formato ou quantity passar a ser string.
Quando uma quebra é detectada, o time do provedor pode:
1. Corrigir o provedor para manter compatibilidade
2. Negociar com o consumidor uma nova versão do contrato
3. Usar versionamento de API (ex: /v2/stock/123)
O fluxo completo garante que integrações entre serviços evoluam com segurança, sem surpresas em produção.
Referências
- Documentação oficial do Pact — Guia completo com tutoriais, referência de API e melhores práticas para implementação de testes de contrato
- Pact Broker - Gerenciamento de contratos — Documentação sobre o Pact Broker, versionamento e estratégias de deploy baseadas em contratos
- Testes de contrato com Pact em Node.js - Tutorial — Tutorial prático da IBM mostrando implementação completa em Node.js
- Padrões de matchers no Pact — Referência detalhada sobre matchers flexíveis para campos dinâmicos
- Pact para mensageria assíncrona — Guia oficial sobre testes de contrato para filas e sistemas de mensageria
- Martin Fowler - Testes de contrato — Artigo seminal de Martin Fowler sobre testes de contrato orientados pelo consumidor
- Comparação: Pact vs OpenAPI vs gRPC — Análise comparativa das abordagens de teste de contrato na InfoQ