Estratégias de testes de contrato entre serviços com Pact
1. Fundamentos do Teste de Contrato com Pact
Testes de contrato são uma abordagem de teste que valida a compatibilidade das comunicações entre serviços, focando exclusivamente nas trocas de mensagens (requests/responses) entre consumidor e provedor. Diferentemente de testes end-to-end, que exigem todos os serviços em pé, e de testes de integração, que testam componentes internos, os testes de contrato com Pact operam em um nível mais granular: cada serviço é testado isoladamente, mas as expectativas de comunicação são compartilhadas através de contratos formais.
Os conceitos-chave são:
- Consumidor (Consumer): serviço que faz requisições a outro serviço.
- Provedor (Provider): serviço que responde às requisições.
- Pact: arquivo JSON que descreve as interações esperadas entre consumidor e provedor.
O ciclo de vida do contrato segue três etapas:
1. Geração: o consumidor define as interações esperadas e gera um Pact.
2. Verificação: o provedor reproduz as interações descritas no Pact e valida se consegue atendê-las.
3. Versionamento: os Pacts são armazenados e versionados no Pact Broker, permitindo rastreamento de mudanças.
2. Configuração Inicial do Ambiente Pact
A configuração do ambiente Pact depende da linguagem do projeto. Usaremos Python como exemplo, mas os conceitos se aplicam a outras linguagens (JS, Java, .NET, etc.).
Instalação da biblioteca Pact Python
pip install pact-python
Estrutura de projeto sugerida
meu-projeto/
├── consumer/
│ ├── tests/
│ │ └── test_consumer.py
│ └── consumer_service.py
├── provider/
│ ├── tests/
│ │ └── test_provider.py
│ └── provider_service.py
└── pact_broker/
└── docker-compose.yml
Uso do Pact Broker com Docker
# docker-compose.yml
version: '3'
services:
postgres:
image: postgres
environment:
POSTGRES_USER: pact
POSTGRES_PASSWORD: pact
broker:
image: pactfoundation/pact-broker
ports:
- "9292:9292"
environment:
PACT_BROKER_DATABASE_URL: postgres://pact:pact@postgres/pact
3. Escrevendo Testes de Consumidor com Pact
No teste de consumidor, definimos as interações esperadas com o provedor. Utilizamos matchers para tornar os contratos flexíveis, evitando fragilidade.
Exemplo de teste de consumidor
# consumer/tests/test_consumer.py
import unittest
from pact import Consumer, Provider
pact = Consumer('UserServiceConsumer').has_pact_with(
Provider('UserServiceProvider'),
pact_dir='./pacts'
)
class TestUserConsumer(unittest.TestCase):
def test_get_user(self):
# Define a interação esperada
(pact
.given('user exists')
.upon_receiving('a request for user 1')
.with_request('GET', '/users/1')
.will_respond_with(200, body={
'id': 1,
'name': 'Alice',
'email': 'alice@example.com'
}))
# Executa o mock do provedor
with pact:
from consumer_service import get_user
result = get_user(1)
self.assertEqual(result['name'], 'Alice')
Matchers para evitar fragilidade
from pact.matchers import like, term
(consumer
.given('user exists')
.upon_receiving('a request for user 1')
.with_request('GET', '/users/1')
.will_respond_with(200, body={
'id': like(1),
'name': like('Alice'),
'email': term('alice@example.com', '^[a-z]+@[a-z]+\.[a-z]+$')
}))
4. Verificação de Contratos no Provedor
No provedor, configuramos a verificação para garantir que as interações definidas pelo consumidor são suportadas.
Exemplo de verificação no provedor
# provider/tests/test_provider.py
from pact import Verifier
class TestUserProvider(unittest.TestCase):
def test_verify_pact(self):
verifier = Verifier(provider='UserServiceProvider')
verifier.verify_pacts(
'./pacts/UserServiceConsumer-UserServiceProvider.json',
provider_base_url='http://localhost:5000',
provider_states_url='http://localhost:5000/provider-states'
)
Estados do provedor (provider states)
Os estados permitem simular diferentes cenários de dados no provedor.
# provider/provider_service.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/provider-states', methods=['POST'])
def set_state():
state = request.json['state']
if state == 'user exists':
# Configura o banco de dados para ter o usuário 1
setup_user(1, 'Alice', 'alice@example.com')
return jsonify({'result': 'ok'})
@app.route('/users/<id>')
def get_user(id):
user = find_user_by_id(id)
return jsonify(user)
5. Estratégias de Versionamento e Evolução de Contratos
Gerenciamento de versões no Pact Broker
# Publicar pact no broker
pact-broker publish ./pacts/UserServiceConsumer-UserServiceProvider.json \
--broker-base-url http://localhost:9292 \
--consumer-app-version 1.0.0 \
--tag prod
Estratégias para breaking changes
- Adicionar novos campos: usar matchers
like()para campos opcionais. - Remover campos: criar uma nova versão do contrato e coordenar a migração.
- Alterar tipos: usar
term()para validar o formato, não o tipo exato.
Uso de tags e branches
# Tag para ambiente de desenvolvimento
pact-broker publish pact.json --consumer-app-version 1.1.0 --tag dev
# Tag para produção
pact-broker publish pact.json --consumer-app-version 1.1.0 --tag prod
6. Integração com Pipeline de CI/CD
Automação no GitHub Actions
# .github/workflows/pact.yml
name: Pact Tests
on: [push]
jobs:
consumer-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run consumer tests
run: |
cd consumer
pip install -r requirements.txt
python -m unittest tests/test_consumer.py
- name: Publish pacts
run: |
pact-broker publish consumer/pacts/*.json \
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--consumer-app-version ${{ github.sha }} \
--tag ${{ github.ref_name }}
provider-verification:
needs: consumer-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Start provider
run: |
cd provider
pip install -r requirements.txt
python provider_service.py &
- name: Verify pacts
run: |
pact-broker can-i-deploy \
--pacticipant UserServiceProvider \
--version ${{ github.sha }} \
--to-environment production \
--broker-base-url ${{ secrets.PACT_BROKER_URL }}
Bloqueio de deploys com can-i-deploy
# Comando para verificar compatibilidade antes do deploy
pact-broker can-i-deploy \
--pacticipant UserServiceProvider \
--version 1.0.0 \
--to-environment production \
--broker-base-url http://localhost:9292
7. Boas Práticas e Armadilhas Comuns
Quando o teste de contrato é suficiente
Testes de contrato são ideais para validar a compatibilidade de APIs entre serviços que se comunicam via HTTP, mensageria ou gRPC. Eles são suficientes quando:
- As interações são bem definidas e estáveis.
- Há pouca variação de estado entre ambientes.
- O foco é garantir que mudanças em um serviço não quebrem outros.
Quando complementar com testes end-to-end
Testes end-to-end são necessários quando:
- Há fluxos que envolvem múltiplos serviços encadeados.
- É preciso validar comportamento de timeout, retry e circuit breaker.
- O ambiente de produção tem configurações específicas (load balancers, autenticação).
Evitando acoplamento excessivo com matchers genéricos
# Ruim: matcher muito específico
body={'id': 1, 'name': 'Alice'}
# Bom: matcher flexível
body={'id': like(1), 'name': like('Alice')}
Monitoramento de contratos obsoletos
# Listar pacts não verificados há mais de 30 dias
pact-broker list-pacts --broker-base-url http://localhost:9292 \
--query 'verificationDate < now - 30 days'
Referências
- Documentação oficial do Pact — Guia completo sobre testes de contrato, instalação e configuração do Pact em diversas linguagens.
- Pact Broker Documentation — Documentação detalhada sobre o Pact Broker, versionamento e integração com CI/CD.
- Pact Python GitHub — Repositório oficial da biblioteca Pact para Python, com exemplos e tutorial de uso.
- Testes de Contrato com Pact: Guia Prático — Artigo técnico no Medium com exemplos práticos de implementação de testes de contrato.
- Pact: Estratégias de Versionamento e CI/CD — Artigo de Martin Fowler sobre contratos orientados a consumidores, com ênfase em versionamento e integração contínua.
- Pact Workshop — Workshop oficial do Pact Foundation com exercícios práticos para aprender testes de contrato do básico ao avançado.
- Can I Deploy com Pact — Documentação sobre a ferramenta
can-i-deploypara bloquear deploys incompatíveis.