Todos os artigos
178 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Tutoriais

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.

Testes de contrato entre serviços
COVER · Tutoriais

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.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil