O que é Contract Testing?
Contract Testing é uma abordagem de testes que valida a comunicação entre dois serviços sem necessidade de integrá-los completamente. Diferente dos testes de integração tradicionais, que dependem de ambos os serviços rodando em sincronismo, o contract testing estabelece um "contrato" — uma especificação clara do que cada serviço espera receber e o que promete devolver.
O contrato é como um acordo legal: se o consumidor (cliente) espera um JSON com campos específicos e o produtor (servidor) promete fornecer exatamente isso, ambos devem honrar esse compromisso. Pact é a ferramenta que nos permite definir, registrar e verificar esses contratos de forma automatizada. Isso reduz drasticamente o acoplamento entre serviços e permite que equipes trabalhem independentemente, sabendo que seus contratos serão respeitados.
Por que não usar testes de integração clássicos?
Testes de integração tradicionais requerem que ambos os serviços estejam disponíveis e funcionando corretamente durante a execução. Isso gera problemas: lentidão nos pipelines, dependência de infraestrutura complexa, dificuldade em isolar falhas e impossibilidade de testar serviços que ainda não foram desenvolvidos. Contract testing quebra esse problema: você simula o comportamento esperado e verifica se ambos os lados respeitam o contrato.
Introdução ao Pact e sua arquitetura
Pact é uma ferramenta open-source que implementa contract testing através de interações gravadas entre consumidor e provedor. A ideia central é simples: o consumidor define o que espera receber do provedor, e o provedor verifica se realmente fornece isso. Essas expectativas são gravadas em um arquivo JSON chamado "pact file", que serve como fonte da verdade.
A arquitetura do Pact funciona em duas fases: fase de testes no consumidor e fase de verificação no provedor. Durante a fase do consumidor, você define o comportamento esperado das chamadas HTTP. O Pact coloca um servidor mock no lugar do serviço real, registra as interações e as salva em um arquivo. Depois, durante a fase do provedor, esse arquivo é executado contra o serviço real para garantir que ele honra o contrato.
Instalação e configuração inicial
Para começar com Pact em TypeScript, você precisa instalar as dependências corretas. O ecossistema Pact é rico, mas para TypeScript usaremos @pact-foundation/pact, que é mantido pela equipe oficial.
npm install --save-dev @pact-foundation/pact jest @types/jest ts-jest
npm install --save-dev typescript
Crie um arquivo jest.config.js para configurar o TypeScript:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
moduleFileExtensions: ['ts', 'js']
};
E um tsconfig.json básico:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Testando o Consumidor com Pact
Na perspectiva do consumidor, você é o código cliente que depende de um serviço externo. Seu trabalho é definir exatamente o que você espera desse serviço: quais endpoints você chamará, que métodos HTTP usará, que corpo você enviará e que respostas espera receber.
Estruturando um teste de consumidor
Vamos criar um exemplo prático: imagine um serviço de usuários que precisa consultar dados em um serviço de perfil. O consumidor fará requisições GET para /api/users/{id} e espera um JSON com nome, email e idade.
import { Pact, Interaction } from '@pact-foundation/pact';
import axios from 'axios';
// Classe que representa o cliente do serviço de perfil
class UserProfileClient {
constructor(private baseUrl: string) {}
async getUserProfile(userId: number) {
const response = await axios.get(`${this.baseUrl}/api/users/${userId}`);
return response.data;
}
}
describe('UserProfileClient - Consumer Tests', () => {
const provider = new Pact({
consumer: 'UserService',
provider: 'ProfileService',
port: 8081,
logLevel: 'warn'
});
beforeAll(() => provider.setup());
afterEach(() => provider.verify());
afterAll(() => provider.finalize());
it('should retrieve user profile by ID', async () => {
// Define a interação esperada
await provider.addInteraction({
state: 'user 123 exists',
uponReceiving: 'a request for user profile',
withRequest: {
method: 'GET',
path: '/api/users/123'
},
willRespondWith: {
status: 200,
body: {
id: 123,
name: 'João Silva',
email: 'joao@example.com',
age: 28
}
}
});
// Instancia o cliente apontando para o mock do Pact
const client = new UserProfileClient('http://localhost:8081');
// Faz a chamada
const profile = await client.getUserProfile(123);
// Valida a resposta
expect(profile).toEqual({
id: 123,
name: 'João Silva',
email: 'joao@example.com',
age: 28
});
});
it('should handle user not found', async () => {
await provider.addInteraction({
state: 'user 999 does not exist',
uponReceiving: 'a request for non-existent user',
withRequest: {
method: 'GET',
path: '/api/users/999'
},
willRespondWith: {
status: 404,
body: {
error: 'User not found'
}
}
});
const client = new UserProfileClient('http://localhost:8081');
try {
await client.getUserProfile(999);
fail('Should have thrown an error');
} catch (error: any) {
expect(error.response.status).toBe(404);
expect(error.response.data.error).toBe('User not found');
}
});
});
Entendendo as interações
Cada interação no Pact possui quatro componentes essenciais: um estado (state), uma descrição do que está sendo requisitado, a requisição esperada e a resposta esperada. O estado é importante porque permite testar diferentes cenários — quando o usuário existe, quando não existe, quando há erro no servidor, etc.
A requisição define o método HTTP, o caminho, headers e body (se aplicável). A resposta define o código de status e o corpo da resposta. O Pact grava tudo isso e cria um arquivo chamado pact/UserService-ProfileService.json que será usado posteriormente para validar o servidor.
Verificando o Contrato no Provedor
Depois que os testes do consumidor rodam com sucesso, você tem um arquivo pact que descreve o contrato. Agora é hora de verificar se o provedor (o servidor real) honra esse contrato. Isso é feito através de "testes de verificação" que executam cada interação contra a API real.
Implementando o provedor
Primeiro, vamos criar um servidor Express simples que simula o serviço de perfil:
import express, { Express } from 'express';
export function createProfileServer(): Express {
const app = express();
app.use(express.json());
// Mock database
const users = {
123: { id: 123, name: 'João Silva', email: 'joao@example.com', age: 28 },
456: { id: 456, name: 'Maria Santos', email: 'maria@example.com', age: 32 }
};
app.get('/api/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
const user = users[userId as keyof typeof users];
if (user) {
res.json(user);
} else {
res.status(404).json({ error: 'User not found' });
}
});
return app;
}
Escrevendo testes de verificação
Os testes de verificação são diferentes: você não define interações, apenas verifica se o provedor cumpre as definidas pelo consumidor. O Pact lê o arquivo gerado anteriormente e executa cada interação contra seu servidor real.
import { Verifier } from '@pact-foundation/pact';
import { createProfileServer } from '../server';
describe('ProfileService - Provider Verification', () => {
let server: any;
const port = 3000;
beforeAll(async () => {
const app = createProfileServer();
server = app.listen(port);
});
afterAll(() => {
server.close();
});
it('validates the pact with the consumer', async () => {
const verifier = new Verifier({
providerBaseUrl: `http://localhost:${port}`,
pactFiles: ['./pact/UserService-ProfileService.json'],
providerVersion: '1.0.0'
});
await verifier.verifyProvider();
});
});
Quando este teste rodas, o Pact automaticamente:
1. Lê o arquivo pact gerado pelo consumidor
2. Extrai cada interação (requisição esperada)
3. Faz a requisição contra seu servidor real
4. Compara a resposta real com a resposta esperada
5. Relata se há divergências
Se o servidor responder diferente do esperado, o teste falha imediatamente, alertando que o contrato foi quebrado.
Gerenciando estados do provedor
Em cenários mais complexos, você pode precisar preparar o provedor em diferentes estados. O Pact suporta "provider states" — funções que preparam o banco de dados ou o estado da aplicação antes de executar cada interação.
import { Verifier } from '@pact-foundation/pact';
describe('ProfileService - Provider Verification com States', () => {
let server: any;
const port = 3000;
beforeAll(async () => {
const app = createProfileServer();
server = app.listen(port);
});
afterAll(() => {
server.close();
});
it('validates pact with provider states', async () => {
const verifier = new Verifier({
providerBaseUrl: `http://localhost:${port}`,
pactFiles: ['./pact/UserService-ProfileService.json'],
providerVersion: '1.0.0',
stateHandlers: {
'user 123 exists': async () => {
// Garante que o usuário 123 existe no banco
console.log('Preparando estado: user 123 exists');
},
'user 999 does not exist': async () => {
// Garante que o usuário 999 não existe
console.log('Preparando estado: user 999 does not exist');
}
}
});
await verifier.verifyProvider();
});
});
Fluxo de Trabalho Completo e Boas Práticas
Um fluxo de contract testing maduro envolve múltiplos passos e considerações importantes. Não é apenas escrever testes — é sobre estabelecer uma cultura de contratos e verificação contínua.
Estrutura de diretórios recomendada
projeto/
├── src/
│ ├── clients/
│ │ └── profileClient.ts
│ ├── server.ts
│ └── services/
├── __tests__/
│ ├── consumer/
│ │ └── profileClient.test.ts
│ └── provider/
│ └── profileService.test.ts
├── pact/
│ └── UserService-ProfileService.json
└── jest.config.js
Executando e publicando contratos
Depois que seus testes rodam, você provavelmente quer compartilhar os contratos com a equipe do provedor. Pact oferece um "Broker" — um repositório central onde contratos podem ser publicados e acessados. Isso é especialmente útil em arquiteturas de microsserviços com múltiplas equipes.
# Publicar contrato no Pact Broker
npm run test:consumer
npx pact publish ./pact \
--consumer-app-version=1.0.0 \
--broker-url=https://seu-broker.pact.sh \
--broker-token=seu-token
Validações importantes no contrato
Um contrato bem escrito é específico, mas não frágil. Evite capturar detalhes que mudarão frequentemente. Use matchers do Pact para definir padrões em vez de valores exatos quando apropriado:
import { Matchers } from '@pact-foundation/pact';
await provider.addInteraction({
state: 'user 123 exists',
uponReceiving: 'a request for user profile',
withRequest: {
method: 'GET',
path: '/api/users/123'
},
willRespondWith: {
status: 200,
body: {
id: Matchers.number(123),
name: Matchers.string('João Silva'),
email: Matchers.regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'joao@example.com'),
age: Matchers.number(28)
}
}
});
Matchers permitem validar formatos sem capturar valores específicos. Um regex para email, por exemplo, valida que qualquer email em formato correto será aceito, não apenas "joao@example.com".
Integração com CI/CD
Em um pipeline de continuous integration, você quer rodar testes de consumidor e provedor em paralelo, sempre que código for alterado:
# .github/workflows/contract-tests.yml
name: Contract Tests
on: [push, pull_request]
jobs:
consumer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm run test:consumer
- name: Publish pact
if: success()
run: npm run pact:publish
provider:
runs-on: ubuntu-latest
needs: consumer
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm run test:provider
Conclusão
Contract testing com Pact e TypeScript oferece três benefícios fundamentais que transformam como você trabalha com APIs. Primeiro, desacopla equipes: consumidores e provedores podem desenvolver independentemente, confiando apenas no contrato, sem esperar por serviços prontos. Segundo, acelera feedback: ao contrário de testes de integração que requerem infraestrutura complexa, contract tests rodam rapidamente e isoladamente em qualquer máquina. Terceiro, documenta expectativas: o arquivo pact é documentação viva que sempre reflete a realidade de como dois serviços se comunicam.
O conhecimento prático que você ganhou aqui — estruturar interações no consumidor, implementar verificações no provedor, usar matchers para tornar contratos resilientes — é suficiente para iniciar em projetos reais. A partir daqui, explore integração com Pact Broker para arquiteturas maiores e considere estender esse padrão para todos os seus consumidores de API.