Fala, dev! Tudo certo? 🚀
Chegamos ao quarto artigo da série Testes para Dev!
Nos artigos anteriores aprendemos sobre Testes de Unidade, Testes de Integração e Continuous Integration (CI/CD).
Agora, imagine o seguinte cenário no mundo real:
O time de backend altera o nome de um campo na resposta da API de
user_idparaid. O backend roda os testes dele e passa. O frontend roda os testes dele e passa. Mas quando sobem para produção... KABOOM! 💥 A tela do usuário fica em branco!
Para resolver esse pesadelo sem precisar montar ambientes integrados ultra lentos e caros, surgem os Testes de Contrato.
Hoje vamos aprender passo a passo (baby steps) como instalar, configurar, escrever linha a linha, executar e ler os resultados de um teste de contrato com Pact, entender o fluxo de verificação no backend, lidar com múltiplos consumidores (Web, Mobile, CLI) e hospedar o Pact Broker.
O que é um Teste de Contrato?
Em termos simples: é um acordo formal em arquivo JSON entre dois sistemas.
- Consumidor (Consumer): A aplicação que faz a chamada (ex: Frontend React, App Mobile, CLI).
- Provedor (Provider): A aplicação que responde com os dados (ex: Backend API Express/Java).
No modelo Consumer-Driven Contracts (CDC), o Consumidor diz exatamente o que precisa consumir e gera o contrato. O Provedor pega esse contrato e valida se sua API responde de acordo.
Por que o contrato DEVE ser gerado pelo Consumidor? 💡
Em abordagens tradicionais (Provider-Driven), o Backend define o Swagger/OpenAPI com 50 campos e diz: "Essa é a minha API, se vira!".
O grande problema disso é que o Backend fica com medo constante de refatorar. Se o backend quiser remover um campo antigo que acha que ninguém usa, ele não tem certeza absoluta se o App Mobile ou a Web dependem daquele campo em produção.
No Consumer-Driven Contracts (CDC), a ordem se inverte:
- Foco no uso real: O consumidor gera o contrato declarando apenas os campos que ele realmente consome. Se o React usa 3 campos de um JSON de 50 propriedades, o contrato do React lista apenas esses 3 campos.
- Segurança total para refatoração: Se o Backend quiser apagar os outros 47 campos que nenhum consumidor declarou no contrato, ele pode apagar com 100% de segurança! O CI dele não vai falhar.
- Proteção contra quebras em produção: O Backend só é impedido de fazer o deploy se alterar ou remover um campo que pelo menos um consumidor ativo declarou que necessita.
Como lidar com um Backend que atende MÚLTIPLOS Consumidores (Web, Mobile, CLI)? 📱 💻 🖥️
No mundo real, uma API no Backend (ex: Express.js) quase nunca atende a um único cliente. Ela serve simultaneamente:
ReactWebClient(Frontend Web)iOSMobileApp(App iPhone)AndroidMobileApp(App Android)AdminCLI(Ferramenta de linha de comando)
Como o Pact resolve essa multiplicidade de consumidores sem virar uma bagunça?
┌────────────────────┐
│ ReactWebClient │ ──────┐
└────────────────────┘ │
│
┌────────────────────┐ │ ┌─────────────────────────┐ ┌─────────────────────┐
│ iOSMobileApp │ ──────┼────► │ PACT BROKER │ ───► │ ExpressBackendAPI │
└────────────────────┘ │ │ (Central de Contratos)│ │ (Valida TODOS os │
│ └─────────────────────────┘ │ contratos no CI) │
┌────────────────────┐ │ └─────────────────────┘
│ AdminCLI │ ──────┘
└────────────────────┘
1. Cada Consumidor gera seu próprio arquivo de contrato isolado:
No projeto do React, o teste declara consumer: 'ReactWebClient', provider: 'ExpressBackendAPI'.
No projeto do iOS, o teste declara consumer: 'iOSMobileApp', provider: 'ExpressBackendAPI'.
No projeto da CLI, o teste declara consumer: 'AdminCLI', provider: 'ExpressBackendAPI'.
Cada um publica o seu contrato .json individual no Pact Broker.
2. O Backend valida TODOS os contratos de uma só vez no CI:
No repositório do Backend Express, o Verifier se conecta ao Pact Broker sem precisar saber quem são os consumidores de cabeça:
import { Verifier } from '@pact-foundation/pact';
describe('Verificação do Provedor de Contratos', () => {
it('valida TODOS os consumidores cadastrados no Pact Broker', async () => {
return new Verifier({
provider: 'ExpressBackendAPI',
providerBaseUrl: 'http://localhost:3001',
pactBrokerUrl: 'http://localhost:9292',
// O Verifier busca automaticamente os contratos ativos de TODOS os consumidores!
publishVerificationResult: true,
providerVersion: process.env.GITHUB_SHA
}).verifyProvider();
});
});
Quando o Backend roda a suite de testes, o Pact lê o contrato da Web, do iOS, do Android e da CLI, e faz requisições de teste para validar todos eles em lote.
3. A Matriz de Compatibilidade e o can-i-deploy 🛑
O Pact Broker mantém uma tabela chamada Matrix de Compatibilidade.
Imagine que o time de iOS lançou o app v2.0 na App Store que exige o campo cpf. Se o Backend tentar subir uma alteração na API que remove o campo cpf, o comando can-i-deploy no CI do Backend vai avisar:
🛑 DEPLOY BLOQUEADO! O Backend v3.1.0 quebra o contrato com o
iOSMobileAppv2.0.0 que está atualmente em produção na App Store!
Isso impede que uma alteração no backend derrube a versão do app mobile que já está instalada no celular dos seus clientes!
Quando USAR (e quando NÃO usar) Testes de Contrato? 🎯
Nem todo projeto precisa de teste de contrato. Saber a hora certa de adotar essa ferramenta economiza tempo de arquitetura:
✅ Quando USAR:
- Microsserviços e APIs distribuídas: Quando múltiplos serviços dependem de endpoints mantidos por equipes diferentes.
- Frontend e Backend desacoplados (React / Mobile vs. Node / Java / Go): Quando o time de Frontend consome APIs e precisa ter certeza de que o backend não quebrou contratos no deploy.
- Desenvolvimento em Paralelo: O time de Frontend pode gerar o contrato e continuar construindo a UI antes mesmo do Backend terminar a API real.
- Substituição de Ambientes Integrados Pesados: Quando o ambiente de staging/homologação é instável, caro de manter e lento.
🛑 Quando NÃO usar:
- Monólitos no mesmo repositório: Se o Frontend e o Backend rodam na mesma base de código em memória, testes de unidade e integração simples resolvem com menos complexidade.
- APIs Públicas de Terceiros sem controle (ex: Stripe, Twitter, GitHub): Você não controla o servidor do Stripe para rodar a validação do contrato. Nesses casos, crie adapters/wrappers locais no seu código e use testes de integração com Stubs.
Ponto fundamental: Pact vs Jest (Quem faz o quê?) 🤝
Antes de instalar as bibliotecas, precisamos deixar uma coisa muito clara:
O Pact NÃO é um executor de testes (test runner). Ele precisa do Jest para rodar!
- O Jest (Test Runner): É o motor do teste. Ele organiza os blocos de código (
describe,it), executa as validações (expect) e avisa o terminal/CI se o teste passou ou falhou. - O Pact (Contract Framework): É a ferramenta que sobe um servidor HTTP mock temporário, intercepta as chamadas do cliente, valida a estrutura do JSON e gera o arquivo do contrato
.jsonno disco.
Você também poderia usar o Vitest ou Mocha no lugar do Jest, mas neste tutorial usaremos a combinação mais popular do mercado: Jest + Pact.
PARTE 1: O Lado do Consumidor (Frontend React) ⚛️
Passo 1: Instalação das Dependências no Frontend via NPM
No terminal do seu projeto Frontend/Consumidor, instale as dependências de desenvolvimento:
npm install --save-dev @pact-foundation/pact jest ts-jest @types/jest
Passo 2: Configurando o Jest para TypeScript (jest.config.js)
Se você rodar o Jest diretamente em um arquivo .ts contendo sintaxe de ESM (import/export), o Node lançará erro. Crie o arquivo jest.config.js na raiz:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
E no seu tsconfig.json:
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"types": ["jest", "node"]
}
}
Passo 3: Configurando os Scripts no package.json
Adicione o script no package.json:
{
"name": "meu-frontend-react",
"version": "1.0.0",
"scripts": {
"test": "jest",
"test:contract": "jest --testMatch='**/*.contract.spec.ts'"
}
}
Passo 4: Escrevendo o Teste do Consumidor (userApi.contract.spec.ts)
No Frontend, temos a função que chama a API (src/services/userApi.ts):
export interface UserProfile {
id: string;
name: string;
email: string;
}
export async function getUserProfile(baseUrl: string, id: string): Promise<UserProfile> {
const response = await fetch(`${baseUrl}/api/users/${id}`);
if (!response.ok) {
throw new Error('Falha ao buscar usuário');
}
return response.json();
}
Agora escrevemos o teste de contrato (src/services/userApi.contract.spec.ts):
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { getUserProfile } from './userApi';
// 1. Instanciamos o Pact definindo quem é o Consumidor e quem é o Provedor
const provider = new PactV3({
consumer: 'ReactFrontendApp',
provider: 'ExpressBackendAPI',
dir: './pacts'
});
describe('Contrato: React App -> Express API', () => {
it('deve retornar o perfil do usuário no formato esperado pelo React', async () => {
// 2. Definimos a expectativa do contrato
provider
.given('Usuário diego-123 cadastrado')
.uponReceiving('Requisição de busca de perfil GET /api/users/diego-123')
.withRequest({
method: 'GET',
path: '/api/users/diego-123'
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: MatchersV3.like('diego-123'),
name: MatchersV3.like('Diego Borges'),
email: MatchersV3.like('diego@email.com')
}
});
// 3. Executamos a chamada contra o servidor mock temporário do Pact
await provider.executeTest(async (mockServer) => {
const user = await getUserProfile(mockServer.url, 'diego-123');
expect(user.name).toBe('Diego Borges');
expect(user.email).toBe('diego@email.com');
});
});
});
Passo 5: Executando o Teste do Consumidor e Gerando o Contrato
No terminal do Frontend, execute:
npm run test:contract
Resultado de Sucesso no Frontend 🟢:
PASS src/services/userApi.contract.spec.ts
Contrato: React App -> Express API
✓ deve retornar o perfil do usuário no formato esperado pelo React (180 ms)
[Pact] Pact file written to ./pacts/ReactFrontendApp-ExpressBackendAPI.json
🎉 O contrato JSON foi gerado em ./pacts/ReactFrontendApp-ExpressBackendAPI.json!
PARTE 2: O Lado do Backend (Provedor Express.js) 🟢
Agora vamos ver passo a passo como construir e rodar o teste de contrato no repositório do Backend.
Passo 1: Instalação no Repositório do Backend
No terminal do seu projeto Backend (ex: sua API em Node/Express), instale as ferramentas de teste:
npm install --save-dev @pact-foundation/pact jest ts-jest @types/jest
Passo 2: A API Express (src/server.js)
Imagine que a sua API Express tem a rota de busca de usuários:
const express = require('express');
const app = express();
app.get('/api/users/:id', (req, res) => {
res.json({
id: req.params.id,
name: 'Diego Borges',
email: 'diego@email.com'
});
});
module.exports = app;
Passo 3: Escrevendo o Teste de Verificação do Provedor (src/provider.contract.spec.ts)
Crie o arquivo de verificação no projeto Backend:
import { Verifier } from '@pact-foundation/pact';
import app from './server';
import http from 'http';
describe('Validação do Provedor Express contra os Contratos dos Consumidores', () => {
let server: http.Server;
// 1. Antes dos testes, subimos o servidor Express real numa porta local de teste (ex: 3001)
beforeAll((done) => {
server = app.listen(3001, () => done());
});
// 2. Após os testes, encerramos o servidor
afterAll((done) => {
server.close(() => done());
});
it('deve garantir conformidade com o contrato do ReactFrontendApp', async () => {
// 3. O Verifier lê o contrato JSON e dispara requisições HTTP reais contra localhost:3001
const output = await new Verifier({
provider: 'ExpressBackendAPI',
providerBaseUrl: 'http://localhost:3001',
// Apontamos para o arquivo gerado pelo Frontend (ou URL do Pact Broker)
pactUrls: ['./pacts/ReactFrontendApp-ExpressBackendAPI.json']
}).verifyProvider();
console.log('Resultado da verificação do contrato:', output);
});
});
Como o Provedor (Backend) Interage com o Contrato Gerado? 🔍
É comum surgir a dúvida: Como o backend lê o arquivo .json e como a mágica acontece por baixo dos panos?
O teste no Backend funciona através de um fluxo em 5 etapas executado pelo Verifier:
┌─────────────────┐ ┌────────────────────┐ ┌──────────────────────┐
│ 1. Leitura do │ ────► │ 2. Subida do App │ ────► │ 3. Requisição HTTP │
│ Contrato JSON │ │ Express (Porta) │ │ Real no Endpoint │
└─────────────────┘ └────────────────────┘ └──────────┬───────────┘
│
┌─────────────────┐ ┌────────────────────┐ │
│ 5. Resultado │ ◄──── │ 4. Comparação de │ ◄────────────────┘
│ PASS ou FAIL │ │ Resposta com JSON │
└─────────────────┘ └────────────────────┘
- Leitura e Parse do JSON: O
Verifierabre o arquivoReactFrontendApp-ExpressBackendAPI.json(localmente ou via Pact Broker) e extrai a lista de interações esperadas (ex:GET /api/users/diego-123). - Subida do Servidor Real: O bloco
beforeAllinicia a sua aplicação Express real na porta local de teste (http://localhost:3001). - Disparo da Requisição HTTP Real: O
Verifieratua como um cliente HTTP cliente real e envia a requisiçãoGET http://localhost:3001/api/users/diego-123diretamente para a sua API Express. - Comparação Campo a Campo: O
Verifiercaptura a resposta devolvida pelo Express e compara com o contrato:- O código de status foi
200? - O header
Content-Typeéapplication/json? - Os campos
id,nameeemailestão presentes no corpo do JSON e têm os tipos esperados?
- O código de status foi
- Report de Verificação: Se todos os campos baterem, o teste passa 🟢. Se algum campo foi alterado ou removido pelo backend, o
Verifieraponta o erro exato e falha a execução com código de saída 1 (interrompendo a pipeline de CI).
Passo 4: Executando a Verificação no Backend
Adicione o script no package.json do Backend:
{
"scripts": {
"test:contract:provider": "jest --testMatch='**/*.provider.contract.spec.ts'"
}
}
No terminal do Backend, rode:
npm run test:contract:provider
Resultado de Sucesso no Backend 🟢:
PASS src/provider.contract.spec.ts
Validação do Provedor Express contra os Contratos dos Consumidores
✓ deve garantir conformidade com o contrato do ReactFrontendApp (450 ms)
Verifying a pact between ReactFrontendApp and ExpressBackendAPI
Given Usuário diego-123 cadastrado
GET /api/users/diego-123
returns a response which
has status code 200 (OK)
includes headers "Content-Type" with value "application/json"
has a matching body (OK)
Como Hospedar e Compartilhar os Contratos (Pact Broker) 🌐
Em projetos do mundo real com repositórios Git e times de desenvolvimento separados, não compartilhamos o arquivo .json copiando manualmente para a máquina do colega ou enviando via Slack.
Para resolver o compartilhamento de contratos, existe o Pact Broker.
O que é o Pact Broker?
O Pact Broker é um servidor central (hub de contratos) open-source que armazena, versiona e serve os arquivos de contrato em uma API REST com painel visual interativo.
Existem duas formas principais de utilizar:
- PactFlow (Gerenciado / Cloud SaaS): A versão cloud pronta para uso mantida pelos criadores do Pact (com plano gratuito para times pequenos).
- Pact Broker Self-Hosted (Docker / Grátis): Subir o Pact Broker na sua própria infraestrutura em nuvem usando Docker Compose.
Como subir o Pact Broker Self-Hosted via Docker Compose 🐳
Para rodar o seu próprio Pact Broker localmente ou no servidor da empresa, crie o arquivo docker-compose.yml:
version: '3'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: pactbroker
POSTGRES_PASSWORD: pactbrokerpassword
POSTGRES_DB: pactbroker
ports:
- "5432:5432"
pact-broker:
image: pactfoundation/pact-broker:latest
ports:
- "9292:9292"
environment:
PACT_BROKER_DATABASE_USERNAME: pactbroker
PACT_BROKER_DATABASE_PASSWORD: pactbrokerpassword
PACT_BROKER_DATABASE_HOST: postgres
PACT_BROKER_DATABASE_NAME: pactbroker
PACT_BROKER_BASE_URL: "http://localhost:9292"
depends_on:
- postgres
Suba os contêineres executando:
docker compose up -d
Acesse no seu navegador: http://localhost:9292. Você verá a interface visual do seu Pact Broker!
Como publicar contratos do Frontend no Pact Broker (no CI/CD)
No pipeline de CI do Frontend (ex: GitHub Actions), após os testes passarem e o .json ser gerado, publicamos o contrato no Pact Broker usando o CLI oficial @pact-foundation/pact-cli:
npx @pact-foundation/pact-cli pact-broker publish ./pacts \
--consumer-app-version=$GITHUB_SHA \
--branch=$GITHUB_REF_NAME \
--broker-base-url=http://localhost:9292
Como o Backend consome do Pact Broker em vez do arquivo local
No repositório do Backend, basta atualizar a propriedade pactUrls para pactBrokerUrl no Verifier:
import { Verifier } from '@pact-foundation/pact';
describe('Validação via Pact Broker', () => {
it('deve validar o contrato baixado do Pact Broker', () => {
return new Verifier({
provider: 'ExpressBackendAPI',
providerBaseUrl: 'http://localhost:3001',
pactBrokerUrl: process.env.PACT_BROKER_URL || 'http://localhost:9292',
// Em produção, passe o token de autenticação:
// pactBrokerToken: process.env.PACT_BROKER_TOKEN
}).verifyProvider();
});
});
Dessa forma, sempre que o Frontend publica uma nova versão de contrato no Broker, a próxima execução do CI do Backend vai buscar automaticamente o contrato atualizado e validar a API antes de autorizar o deploy!
Resumo dos Benefícios 🎯
- Feedback instantâneo: Descubra quebras de contrato no CI em segundos.
- Chega de ambientes pesados: Não precisa subir Docker com 10 microsserviços integrados para validar APIs.
- Confiança para refatorar: O backend pode refatorar com segurança desde que respeite os contratos ativos.
O que vem por aí na série "Testes para Dev"? 🪝
Seus testes cobrem unidades, integrações, CI e contratos de API. Mas me responde: como ter certeza de que seus testes realmente testam o código e não são apenas "cobertura cosmética" para bater meta?
No Capítulo #5, vamos falar sobre Testes de Mutação com Stryker! Vamos aprender a soltar "mutantes" no nosso código para ver se a suíte de testes é forte o suficiente para exterminá-los.
Deixe seu comentário e até o próximo post! 🚀