Testes de contrato entre serviços
Testes de contrato entre serviços: entenda os benefícios, limitações e quando vale a pena adotá-los na sua arquitetura distribuída.
O campo que sumiu sem ninguém avisar
Sexta à tarde, time de pagamentos renomeou um campo de transaction_id para txn_id na resposta da API deles. Deploy tranquilo, testes do serviço passando, ninguém reclamou. Na segunda, o serviço de faturamento — que consome essa API — começou a gravar null em todas as transações. Nenhum erro, nenhum 500, só dado silenciosamente quebrado. Os testes de integração não pegaram porque rodavam contra um mock que ainda tinha o campo antigo. Isso é o problema clássico que contract testing existe para resolver: o provider muda o payload e o consumer descobre em produção.
Aqui eu explico o que é teste de contrato, como ele difere de validação de schema e de E2E, e quando vale a pena. É um complemento direto de testar APIs de forma automatizada — onde os testes de API garantem que cada serviço funciona, o contrato garante que eles continuam concordando entre si.
O que um teste de contrato realmente testa
Um contrato é um acordo explícito sobre o formato da comunicação entre dois serviços: quais campos a requisição tem, quais campos a resposta devolve, tipos, obrigatoriedade. O teste de contrato verifica que provider e consumer continuam honrando esse acordo — independentemente, sem precisar subir os dois juntos.
A sacada é que o contrato é verificado dos dois lados, em momentos separados. O consumer declara o que espera. O provider prova que entrega exatamente aquilo. Se um lado quebra o acordo, o teste falha na hora do build daquele serviço, não em produção três dias depois.
Consumer-driven contracts (estilo Pact)
A abordagem que pegou tração é o consumer-driven contract, popularizado pelo Pact. A lógica é: quem dita o contrato é o consumer, porque é ele quem sofre quando a resposta muda. O consumer escreve um teste que descreve a interação esperada, e esse teste gera um arquivo de contrato (o "pact").
{
"description": "GET /transactions/42 returns a transaction",
"request": { "method": "GET", "path": "/transactions/42" },
"response": {
"status": 200,
"body": {
"transaction_id": "string",
"amount": "number",
"currency": "string"
}
}
}
Esse contrato é publicado num broker, e o provider precisa rodar a provider verification: ele pega o contrato de cada consumer e roda as requisições contra a implementação real, conferindo que a resposta bate. Se o time de pagamentos tivesse isso, o build deles teria falhado no exato commit que renomeou transaction_id, porque o contrato do faturamento ainda esperava o nome antigo. O bug nunca chegaria na segunda-feira.
O ganho não é só pegar a quebra — é pegar antes do deploy, no CI do lado certo, com uma mensagem clara dizendo qual consumer você ia quebrar.
Contrato não é validação de schema
Aqui mora muita confusão. Validação de schema confere se um payload específico bate com uma estrutura esperada — você pode pegar uma resposta e validar contra um JSON Schema usando um Validador de JSON Schema para confirmar que os tipos e campos obrigatórios estão certos. Isso é ótimo, mas é um teste pontual de um payload.
O contrato é mais do que isso: ele amarra duas pontas. O schema diz "esse JSON é válido". O contrato diz "o serviço A espera exatamente isto, e o serviço B promete entregar exatamente isto, e os dois builds vão falhar se um quebrar a promessa". Schema é estrutura; contrato é acordo bilateral verificado nos dois CIs. Na prática você usa schema dentro do contrato — a definição do corpo esperado costuma ser, no fundo, um schema. Mas a validação isolada não te avisa que você quebrou o consumer; ela só te diz se um payload que você já tem em mãos está bem-formado.
Contrato versus E2E
A alternativa óbvia para garantir que os serviços conversam é o teste end-to-end: sobe tudo, dispara uma requisição real, confere o resultado final. Funciona, mas tem dois problemas crônicos — é lento (subir N serviços, banco, filas) e é flaky (qualquer instabilidade de rede ou timing derruba o teste por motivo que não é bug). Um suite de E2E que demora 40 minutos e falha 1 em cada 5 execuções por motivo aleatório é um suite que o time aprende a ignorar.
Contrato resolve a parte de "os serviços concordam no formato" de forma rápida e determinística, sem subir nada junto. Cada lado testa contra o contrato isoladamente, em milissegundos. Isso não substitui E2E para fluxos críticos de negócio de ponta a ponta — mas tira do E2E a responsabilidade de pegar quebra de formato, que é justamente o caso mais comum e mais barato de pegar cedo.
Quando vale e quando é exagero
Contrato brilha em arquitetura de microserviços com vários times deployando independentemente. É exatamente aí que a quebra silenciosa acontece: times diferentes, ciclos de deploy diferentes, ninguém com visão completa de quem consome o quê.
- Vale: múltiplos serviços, múltiplos times, deploys independentes, integrações HTTP/mensageria entre fronteiras de time.
- Exagero: monolito, ou um único time dono de provider e consumer que sempre deployam juntos. Aí o type system e os testes de integração normais já cobrem; adicionar broker, contrato e verification é cerimônia sem retorno.
A regra prática: contrato compensa quando a fronteira entre os serviços é também uma fronteira entre times ou entre ciclos de deploy. Quando provider e consumer vivem no mesmo repositório e sobem juntos, o compilador é o seu contrato.
Perguntas frequentes
Contract testing substitui testes de integração?
Não. Ele cobre uma fatia específica: a compatibilidade de formato entre provider e consumer. Você ainda precisa de testes unitários, de integração e de E2E para os fluxos críticos. O contrato tira do E2E o fardo de pegar quebra de payload, mas não testa lógica de negócio ponta a ponta.
Preciso de uma ferramenta tipo Pact ou dá pra fazer na mão?
Dá pra começar na mão com um schema compartilhado validado nos dois lados, mas você perde a parte mais valiosa: a verificação automática do provider contra os contratos reais de todos os consumers via broker. Ferramentas como Pact existem para gerenciar essa coreografia — qual versão de contrato cada consumer publicou e contra qual versão de provider foi verificada.
Consumer-driven ou provider-driven?
Consumer-driven é o padrão porque é o consumer quem quebra quando a resposta muda — faz sentido ele declarar o que precisa. Provider-driven faz sentido quando o provider é um produto com muitos consumers desconhecidos (uma API pública), aí o provider publica o contrato e os consumers se adaptam.
Contrato pega mudança que quebra comportamento, não só formato?
Não diretamente. Contrato verifica o formato e a presença dos campos. Se o provider mantém amount como number mas passa a devolver o valor em centavos em vez de reais, o contrato continua verde — o tipo não mudou. Para isso você precisa de testes de comportamento; o contrato cobre estrutura, não semântica.
O que isso muda na sua arquitetura
Contract testing não é mais uma camada de teste por capricho — é a resposta direta para a quebra silenciosa entre serviços que times independentes inevitavelmente causam. Ele pega o erro no CI do lado certo, antes do deploy, com mensagem clara, e faz isso rápido e sem flakiness, ao contrário do E2E. Use schema para validar payloads isolados, contrato para amarrar provider e consumer, e E2E só para os poucos fluxos críticos ponta a ponta que valem o custo. E lembre da regra: se a fronteira do serviço não é fronteira de time nem de deploy, provavelmente é exagero — guarde a ceremônia para onde ela paga o aluguel.
- 01 Licenças MIT, Apache e GPL: diferenças práticas para devs MIT, Apache 2.0 e GPL não são a mesma coisa. Entenda permissiva vs copyleft, proteção de patentes e compatibilidade antes de colocar código em produção.
- 02 Como desfazer alterações no Git com segurança: reset, revert, restore e reflog Qual comando usar para desfazer alterações no Git em cada situação — reset, revert, restore, stash e reflog — sem perder trabalho.