<h2>O que é Contract Testing?</h2>
<p>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.</p>
<p>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.</p>
<h3>Por que não usar testes de integração clássicos?</h3>
<p>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.</p>
<h2>Introdução ao Pact e sua arquitetura</h2>
<p>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.</p>
<p>A arquitetura do Pact funciona em duas fases: <strong>fase de testes no consumidor</strong> e <strong>fase de verificação no provedor</strong>. 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.</p>
<h3>Instalação e configuração inicial</h3>
<p>Para começar com Pact em TypeScript, você precisa instalar as dependências corretas. O ecossistema Pact é rico, mas para TypeScript usaremos <code>@pact-foundation/pact</code>, que é mantido pela equipe oficial.</p>
<pre><code class="language-bash">npm install --save-dev @pact-foundation/pact jest @types/jest ts-jest
npm install --save-dev typescript</code></pre>
<p>Crie um arquivo <code>jest.config.js</code> para configurar o TypeScript:</p>
<pre><code class="language-javascript">module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['/__tests__//*.test.ts'],
moduleFileExtensions: ['ts', 'js']
};</code></pre>
<p>E um <code>tsconfig.json</code> básico:</p>
<pre><code class="language-json">{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}</code></pre>
<h2>Testando o Consumidor com Pact</h2>
<p>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.</p>
<h3>Estruturando um teste de consumidor</h3>
<p>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 <code>/api/users/{id}</code> e espera um JSON com nome, email e idade.</p>
<pre><code class="language-typescript">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');
}
});
});</code></pre>
<h3>Entendendo as interações</h3>
<p>Cada interação no Pact possui quatro componentes essenciais: um <strong>estado</strong> (state), uma descrição do que está sendo requisitado, a <strong>requisição esperada</strong> e a <strong>resposta esperada</strong>. O estado é importante porque permite testar diferentes cenários — quando o usuário existe, quando não existe, quando há erro no servidor, etc.</p>
<p>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 <code>pact/UserService-ProfileService.json</code> que será usado posteriormente para validar o servidor.</p>
<h2>Verificando o Contrato no Provedor</h2>
<p>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.</p>
<h3>Implementando o provedor</h3>
<p>Primeiro, vamos criar um servidor Express simples que simula o serviço de perfil:</p>
<pre><code class="language-typescript">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;
}</code></pre>
<h3>Escrevendo testes de verificação</h3>
<p>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.</p>
<pre><code class="language-typescript">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();
});
});</code></pre>
<p>Quando este teste rodas, o Pact automaticamente:</p>
<ol>
<li>Lê o arquivo pact gerado pelo consumidor</li>
<li>Extrai cada interação (requisição esperada)</li>
<li>Faz a requisição contra seu servidor real</li>
<li>Compara a resposta real com a resposta esperada</li>
<li>Relata se há divergências</li>
</ol>
<p>Se o servidor responder diferente do esperado, o teste falha imediatamente, alertando que o contrato foi quebrado.</p>
<h3>Gerenciando estados do provedor</h3>
<p>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.</p>
<pre><code class="language-typescript">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();
});
});</code></pre>
<h2>Fluxo de Trabalho Completo e Boas Práticas</h2>
<p>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.</p>
<h3>Estrutura de diretórios recomendada</h3>
<pre><code>projeto/
├── src/
│ ├── clients/
│ │ └── profileClient.ts
│ ├── server.ts
│ └── services/
├── __tests__/
│ ├── consumer/
│ │ └── profileClient.test.ts
│ └── provider/
│ └── profileService.test.ts
├── pact/
│ └── UserService-ProfileService.json
└── jest.config.js</code></pre>
<h3>Executando e publicando contratos</h3>
<p>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.</p>
<pre><code class="language-bash"># 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</code></pre>
<h3>Validações importantes no contrato</h3>
<p>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:</p>
<pre><code class="language-typescript">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)
}
}
});</code></pre>
<p>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".</p>
<h3>Integração com CI/CD</h3>
<p>Em um pipeline de continuous integration, você quer rodar testes de consumidor e provedor em paralelo, sempre que código for alterado:</p>
<pre><code class="language-yaml"># .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</code></pre>
<h2>Conclusão</h2>
<p>Contract testing com Pact e TypeScript oferece três benefícios fundamentais que transformam como você trabalha com APIs. Primeiro, <strong>desacopla equipes</strong>: consumidores e provedores podem desenvolver independentemente, confiando apenas no contrato, sem esperar por serviços prontos. Segundo, <strong>acelera feedback</strong>: ao contrário de testes de integração que requerem infraestrutura complexa, contract tests rodam rapidamente e isoladamente em qualquer máquina. Terceiro, <strong>documenta expectativas</strong>: o arquivo pact é documentação viva que sempre reflete a realidade de como dois serviços se comunicam.</p>
<p>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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://docs.pact.foundation/" target="_blank" rel="noopener noreferrer">Documentação Oficial do Pact</a></li>
<li><a href="https://github.com/pact-foundation/pact-js" target="_blank" rel="noopener noreferrer">Pact JS Library - GitHub</a></li>
<li><a href="https://martinfowler.com/bliki/ContractTest.html" target="_blank" rel="noopener noreferrer">Contract Testing Guide - Martin Fowler</a></li>
<li><a href="https://docs.pact.sh/" target="_blank" rel="noopener noreferrer">Pact Broker - Compartilhamento de Contratos</a></li>
<li><a href="https://jestjs.io/docs/getting-started#using-typescript" target="_blank" rel="noopener noreferrer">TypeScript + Jest Best Practices</a></li>
</ul>
<p><!-- FIM --></p>