Em uma arquitetura monolítica, "testar a integração entre módulos" significa subir o monolito e exercitar caminhos completos. Funciona, embora seja lento. Em microsserviços, essa abordagem rapidamente desmorona: você precisa subir 5, 10, 20 serviços, com seus bancos, filas, dependências externas. O tempo de feedback explode. A flakiness explode. A combinação de versões para testar explode. Em breve, "testes de integração entre serviços" vira algo que ninguém consegue rodar localmente, que demora horas no CI, e que falha por motivos não relacionados à mudança que você fez. Times maduros desistem da abordagem ingênua.
Contract testing é a alternativa que resolve esse problema. Em vez de verificar "estes serviços funcionam juntos quando subidos", você verifica "cada serviço cumpre o contrato que outros esperam dele". O contrato é um artefato compartilhado entre consumidor e provedor — schemas, exemplos de requisição e resposta, regras de validação. Cada lado é testado contra esse contrato em isolamento, sem precisar do outro presente. Quando os dois passam contra o mesmo contrato, a integração está formalmente verificada — mesmo sem nunca terem conversado de fato.
O problema concreto
Suponha que você tem dois serviços: OrderService e
PaymentService. OrderService chama
PaymentService para autorizar pagamento. Sem contract
testing, suas opções são:
-
Mock estático em
OrderService: você escreve um stub doPaymentClientque retorna respostas pré-definidas. Testes rápidos. Mas o mock pode estar desatualizado —PaymentServicemudou o formato da resposta há três sprints e ninguém atualizou o mock. Testes verdes; produção quebra. -
E2E real: subir
OrderService,PaymentService, banco de cada um, infraestrutura comum. Caro. Lento. E quando algo falha, qual dos dois está errado? -
Ambiente compartilhado de staging: um único
ambiente onde todos os serviços estão atualizados. Mas a sequência
de deploy importa, mudanças incompatíveis quebram o ambiente
inteiro, e você não pode testar mudança no
OrderServicesem uma versão correspondente doPaymentService.
Cada uma dessas tem custos reais. Contract testing oferece um quarto caminho: defina o contrato uma vez, teste cada lado contra ele.
Os dois sabores — schema vs consumer-driven
Há duas filosofias principais de contract testing, com diferenças importantes.
Schema-based — provider-driven
O provedor publica um schema (OpenAPI, Protobuf, JSON Schema, gRPC reflection). Consumidores baixam, geram clientes, e testam contra esse schema. Validação acontece via:
- Provedor verifica que sua implementação está conforme o schema (request/response shapes batem).
- Consumidor verifica que seu uso é conforme o schema (campos usados existem, tipos batem).
Vantagens: o schema é único e canônico. Nova consumidores aparecem, pegam o schema, geram cliente, funciona. Desvantagens: o consumidor pode usar uma fração minúscula do schema; mudanças em campos que ele não usa não deveriam afetá-lo, mas se o schema dele incluir validação rígida, qualquer mudança quebra. Esse é o problema clássico de "schema descreve tudo o que o provedor pode fazer", não "o que o consumidor precisa".
Consumer-driven contracts (CDC) — Pact
A inversão: cada consumidor escreve um pact descrevendo o que ele espera do provedor, em forma de exemplos concretos. O provedor recebe esses pacts e verifica que cumpre cada um deles.
- Consumidor escreve testes contra um mock local (parte do framework Pact).
-
Esses testes geram um arquivo
pact.json: registra request → response esperada, em exemplos. - Pact files são publicados num "Pact Broker" (ou comitados em repositório).
- Provedor pega cada pact e replay: para cada request descrito, verifica que sua resposta bate com a esperada.
Vantagens: o contrato é dirigido pelo que realmente é usado. Mudanças em campos não-utilizados pelo consumidor não quebram seu pact. Desvantagens: você só descobre quebra quando alguém roda verify; consumidores precisam ser disciplinados em manter pacts; relação consumer→provider é explícita (não vale para "qualquer cliente futuro").
Pact é o framework dominante para CDC. OpenAPI dominante para schema-based. Times maduros frequentemente usam ambos: OpenAPI como schema canônico do provedor; Pact entre consumidores específicos com necessidades específicas.
Como Pact funciona na prática
Vou ilustrar com o caso OrderService ↔
PaymentService.
Lado do consumidor
No OrderService, você escreve um teste que define o que
ele espera:
// C# com PactNet
[Fact]
public async Task AuthorizesPayment_When_Valid() {
pactBuilder.UponReceiving("a payment authorization request")
.Given("user has valid card")
.WithRequest(HttpMethod.Post, "/payments/authorize")
.WithJsonBody(new {
order_id = "abc-123",
amount = 150.00m,
currency = "BRL"
})
.WillRespond()
.WithStatus(200)
.WithJsonBody(new {
authorization_id = Match.Type("auth-xyz"),
status = "authorized",
authorized_at = Match.Iso8601DateTime()
});
await pactBuilder.VerifyAsync(async ctx => {
var client = new PaymentClient(ctx.MockServerUri);
var result = await client.Authorize(
new AuthorizeRequest {
OrderId = "abc-123",
Amount = 150.00m,
Currency = "BRL"
});
result.Status.Should().Be("authorized");
result.AuthorizationId.Should().NotBeNullOrEmpty();
});
}
Este teste:
- Sobe um mock HTTP local que simula
PaymentService. - Configura o mock para responder específicamente ao request descrito.
- Roda o
PaymentClientreal contra o mock. - Verifica que ele entende a resposta corretamente.
- Gera o arquivo
pact.jsonregistrando essa expectativa.
Note os Match.Type e Match.Iso8601DateTime:
você não exige valor exato de campo gerado pelo provedor —
exige tipo ou formato. O provedor pode mudar o
authorization_id a cada chamada; o consumidor só precisa
que seja string não-vazia.
Lado do provedor
No PaymentService, você roda verificação contra o pact:
// PactNet — verifier no provedor
[Fact]
public void Verify_OrderServiceContract() {
var verifier = new PactVerifier(new PactVerifierConfig());
verifier
.ServiceProvider("PaymentService", new Uri("http://localhost:5001"))
.WithFileSource(new FileInfo("../pacts/orderservice-paymentservice.json"))
.WithProviderStateUrl(new Uri("http://localhost:5001/pact/states"))
.Verify();
}
O verifier:
- Lê o pact gerado pelo
OrderService. - Para cada interação, reproduz o request contra o
PaymentServicerodando. - Verifica que a resposta bate com a esperada (estrutura, tipos, campos).
- Falha se algo divergir.
O WithProviderStateUrl aponta para um endpoint
especial que prepara estado antes de cada interação ("user has
valid card" → seed banco com usuário tendo cartão válido). Esse
é o ponto delicado de Pact: provedor precisa expor um caminho
para configurar pre-condições para cada teste.
Pact Broker — orquestração
Em times pequenos, pacts podem ser arquivos commitados em repositórios. Em times maiores, o Pact Broker resolve:
- Consumidores publicam pacts ao Broker no CI.
- Provedores buscam pacts do Broker antes de verificar.
- Broker mantém versões e tags (qual pact da versão X bate com qual provedor da versão Y).
- Broker tem feature "can-i-deploy?" — antes de subir nova versão, pergunta ao Broker se há combinações verificadas.
A "can-i-deploy" é o real ganho de Pact em escala: antes de
deployar PaymentService v2.5 em produção, CI pergunta
"há combinação verificada com cada consumidor de produção?". Se
OrderService em produção é v3.1 e v3.1 não foi
verificado contra v2.5, deploy não rola. É garantia formal de
compatibilidade.
OpenAPI / Schema-based — abordagem complementar
Para APIs HTTP REST, OpenAPI (anteriormente Swagger) é o formato padrão de schema. Define endpoints, métodos, parâmetros, schemas de request/response. Pode ser fonte da verdade ou gerado a partir do código.
Validações úteis:
-
Provedor → schema: ferramentas como
schemathesisouspectralvalidam que a API rodando bate com o que OpenAPI declara. Útil em CI para detectar drift entre código e spec. - Consumidor → schema: gera client tipado a partir do schema. Mudança no schema vira compile error no consumidor. Detecta incompatibilidade na hora.
- Mock servers a partir do schema: ferramentas como Prism geram servidor mock direto do OpenAPI. Útil para consumidor desenvolver antes do provedor estar pronto.
Comparado a Pact, OpenAPI é menos preciso (descreve "tudo o que pode acontecer") mas mais leve (sem coordenação entre serviços durante teste). Para cenários onde o schema é estável e consumidores múltiplos, OpenAPI vence. Para cenários onde poucos consumidores muito acoplados, Pact dá garantia mais forte.
O mesmo nas três linguagens
// Consumidor
var pact = Pact.V3("OrderService", "PaymentService", new PactConfig());
var pactBuilder = pact.WithHttpInteractions();
pactBuilder.UponReceiving("authorization request")
.WithRequest(HttpMethod.Post, "/payments/authorize")
.WithJsonBody(new { order_id = "abc", amount = 100m })
.WillRespond()
.WithStatus(200)
.WithJsonBody(new {
authorization_id = Match.Type("auth-x"),
status = "authorized"
});
// Provedor — verifier
new PactVerifier(new PactVerifierConfig())
.ServiceProvider("PaymentService", new Uri("http://localhost:5001"))
.WithPactBrokerSource(new Uri("https://broker.example.com"),
opts => opts.PublishResults("v1.2.3"))
.Verify();
PactNet é o port oficial de Pact para .NET. Suporta Pact v3 e v4. Integração com xUnit/NUnit é simples. Verifier usa state handlers via HTTP endpoint.
# Consumidor
from pact import Consumer, Provider, Like, EachLike
pact = Consumer('OrderService').has_pact_with(Provider('PaymentService'))
pact.start_service()
(pact
.given('user has valid card')
.upon_receiving('authorization request')
.with_request('POST', '/payments/authorize',
body={'order_id': 'abc', 'amount': 100})
.will_respond_with(200, body={
'authorization_id': Like('auth-x'),
'status': 'authorized'
}))
with pact:
client = PaymentClient(pact.uri)
result = client.authorize(order_id='abc', amount=100)
assert result.status == 'authorized'
# Provedor — verifier (CLI ou Python)
# pact-verifier --provider-base-url=http://localhost:5001 \
# --pact-url=./pacts/orderservice-paymentservice.json
pact-python tem maturidade variada — versão estável funciona
bem para HTTP. CLI pact-verifier independe de
linguagem e frequentemente é mais robusto que SDKs específicos.
// Consumidor
import "github.com/pact-foundation/pact-go/v2/consumer"
mockProvider, _ := consumer.NewV4Pact(consumer.MockHTTPProviderConfig{
Consumer: "OrderService",
Provider: "PaymentService",
})
err := mockProvider.AddInteraction().
Given("user has valid card").
UponReceiving("authorization request").
WithRequest("POST", "/payments/authorize",
func(b *consumer.V4RequestBuilder) {
b.JSONBody(map[string]interface{}{
"order_id": "abc", "amount": 100,
})
}).
WillRespondWith(200, func(b *consumer.V4ResponseBuilder) {
b.JSONBody(matchers.Map{
"authorization_id": matchers.Like("auth-x"),
"status": "authorized",
})
}).
ExecuteTest(t, func(c consumer.MockServerConfig) error {
client := NewPaymentClient(c.URL)
result, err := client.Authorize("abc", 100)
if err != nil { return err }
if result.Status != "authorized" {
t.Fatalf("expected authorized, got %s", result.Status)
}
return nil
})
pact-go v2 é a versão moderna, baseada no core Rust de Pact. API mais verbosa que outras linguagens, mas estável. Verifier rodando contra provedor é direto.
Quando contract testing vale
Contract testing introduz complexidade: mais um sistema (Broker), mais um conceito (provider state), coordenação adicional entre times. O ROI aparece em contextos específicos:
- Microsserviços com fronteiras estáveis: cada serviço tem APIs HTTP/gRPC consumidas por outros serviços internos. Contracts entre eles formalizam o que cada lado depende.
- APIs públicas com clientes externos: clientes móveis, parceiros, bibliotecas SDK. Contract testing protege contra breaking changes acidentais.
- Times distribuídos com pouca coordenação: time de provedor e time de consumidor mudam em ritmos diferentes, possivelmente em fusos diferentes. Pacts no Broker dão sinal objetivo de compatibilidade.
- Releases independentes: cada serviço deploy quando quiser, sem coordenar com outros. Can-i-deploy do Broker é a garantia de que pode.
Quando não vale:
- Monolito ou few-services: dois ou três serviços rodando juntos é mais simples testar via E2E.
- Times altamente acoplados que deployam juntos: se você sempre deploya A e B juntos, contract testing é cerimônia sem ganho.
- APIs de baixo tráfego entre poucos consumidores controlados: combinação test + comunicação manual pode ser suficiente.
Contract testing tem curva de aprendizado significativa. Provider states, matchers, Broker, can-i-deploy — cada um precisa ser compreendido. Times que adotam sem essa preparação frequentemente ficam com Pact mal-configurado, gerando falsos positivos e falsos negativos. Resultado: equipe perde confiança e abandona. Trate adoção como projeto: alguém estuda profundamente, configura protótipo, evangeliza com ROI demonstrado.
Padrões e antipatterns
Bom: matchers em vez de valores exatos
Use Like, EachLike,
regex para campos gerados ou variáveis. Especificar
"id": "auth-12345" exato amarra teste demais —
provedor não pode mudar formato sem quebrar pact.
Bom: state explícito por interação
Cada interação declara seu estado pré-requisito ("user has valid card", "account is locked"). Provedor expõe endpoint para configurar. Mantém testes isolados.
Antipattern: pact gigante com 50 interações
Um pact deveria cobrir o que esse consumidor usa. Não tudo o que o provedor faz. 50 interações em um pact é sinal de que você está usando Pact como schema-based — provavelmente OpenAPI cabe melhor.
Antipattern: pacts gerados manualmente
Pact files devem ser gerados por testes do consumidor. Editar JSON na mão é como editar mock manualmente — viole-se a garantia de que o cliente real consegue consumir o que está no pact.
Antipattern: ignorar verify failures
Provider verifier falha → "ah, é só o ambiente, ignora". Se você ignora, perdeu o valor. Trate verify falhando como o mesmo nível de severidade que CI vermelho.
Como praticar
- Implemente Pact entre dois serviços do projeto. OrderService chama PaymentService. Escreva pact no consumidor; verify no provedor. Ambos passam — quebre o provedor, veja verify falhar.
- Configure can-i-deploy. Suba um Pact Broker local (Docker). Publique pacts. Tente "deployar" provedor sem ter consumidor pact compatível. Veja can-i-deploy bloquear.
-
Compare com OpenAPI. Gere OpenAPI do mesmo
provedor. Use
schemathesiscontra ele. Compare a força das duas abordagens — onde Pact é mais preciso, onde OpenAPI cobre mais.
Referências para aprofundar
- livro Building Microservices (2nd ed.) — Sam Newman (2021).
- livro Microservices Patterns — Chris Richardson (2018).
- artigo Consumer-Driven Contracts — Ian Robinson (Martin Fowler bliki).
- artigo What is Pact? — Pact docs.
- artigo Contract Testing in Practice — ThoughtWorks Tech Radar.
- artigo Schema-Based vs Consumer-Driven Contracts — Pact blog.
- docs Pact Documentation.
- docs OpenAPI Specification.
- docs Schemathesis.
- docs Pact Broker.
- vídeo Contract Testing with Pact — Beth Skurrie.
- vídeo Microservices Testing — Distributed Systems — Cindy Sridharan.