Blog

Testes de contrato para webhooks e APIs: reduzindo regressão em integrações

A quebra mais perigosa em integração não dá erro: ela acontece em silêncio. Um parceiro muda um campo do payload, sobe uma versão da API e seu código continua rodando, só que processando dado errado ou ignorando o que mudou. Você descobre dias depois, pelo cliente. Teste de contrato existe para transformar essa mudança invisível em um teste vermelho no CI, antes do deploy. Este guia mostra como validar payloads de webhook contra schema, gravar contratos de APIs externas que você consome e detectar drift cedo, com profundidade de QA de integrações.

2026-01-24 / Qualidade / 9 min

01

O problema: a quebra que ninguém vê

Integração saudável no deploy não garante integração saudável amanhã. O contrato com o parceiro é vivo: ele renomeia um campo, troca um número por string, adiciona um nível de aninhamento ou versiona a API inteira. Nada disso aparece no seu teste unitário, porque seu mock foi escrito uma vez e congelado. O resultado clássico é a quebra silenciosa: o webhook chega, seu parser não acha o campo esperado, grava null e segue em frente sem erro. Em produção, isso vira pedido sem valor, mensagem sem destinatário ou status que nunca atualiza, e você só percebe quando o cliente reclama.

Teste de contrato ataca exatamente essa cegueira. Em vez de confiar que o payload continua igual, você afirma o formato esperado de forma explícita e executável. Quando o parceiro muda, o teste falha de propósito, no CI, com a mudança apontada campo a campo, e não em produção três dias depois.

02

Consumer-driven, provider e schema validation

Existem três abordagens com nomes parecidos que resolvem problemas diferentes. Escolher errado gera teste que dá trabalho e não pega a regressão certa. A tabela abaixo separa o papel de cada uma no contexto de quem consome um parceiro externo.

AbordagemQuem define o contratoO que pegaQuando usar
Consumer-driven (ex. Pact)O consumidor expressa o que precisa do providerProvider quebra o que algum consumidor usaVocê é o provider e quer evitar quebrar clientes
Provider contractO provider publica o contrato (OpenAPI, AsyncAPI)Consumidor desvia do contrato publicadoParceiro tem spec formal e versionada
Schema validationVoce afirma o schema do payload que recebePayload real diverge do schema esperadoParceiro externo sem spec ou que muda sem avisar

Para quem integra com um parceiro grande (Meta, gateways de pagamento, ERPs), o caso mais comum é o terceiro: você não controla o provider e nem sempre ele publica spec confiável. Então a defesa prática é validar todo payload recebido contra um schema seu e gravar contratos das respostas que você consome para detectar drift.

03

Validar o payload do webhook contra um schema

A primeira linha de defesa é nunca processar um webhook sem antes validar a forma. Com zod você declara o contrato esperado e, no momento que o payload diverge, recebe um erro descritivo apontando o campo, em vez de um null silencioso lá na frente. Abaixo, um schema para o payload de mensagem recebida do WhatsApp Cloud API.

// webhookSchema.js
const { z } = require('zod');

const messageSchema = z.object({
  from: z.string().min(8),
  id: z.string(),
  timestamp: z.string(),
  type: z.enum(['text', 'image', 'audio', 'document', 'interactive']),
  text: z.object({ body: z.string() }).optional(),
});

const valueSchema = z.object({
  messaging_product: z.literal('whatsapp'),
  metadata: z.object({
    display_phone_number: z.string(),
    phone_number_id: z.string(),
  }),
  messages: z.array(messageSchema).optional(),
  statuses: z.array(z.object({ id: z.string(), status: z.string() })).optional(),
});

const webhookSchema = z.object({
  object: z.literal('whatsapp_business_account'),
  entry: z.array(z.object({
    id: z.string(),
    changes: z.array(z.object({
      field: z.literal('messages'),
      value: valueSchema,
    })),
  })).min(1),
});

module.exports = { webhookSchema };
// no handler do webhook
const { webhookSchema } = require('./webhookSchema');

app.post('/webhook', (req, res) => {
  const parsed = webhookSchema.safeParse(req.body);

  if (!parsed.success) {
    // contrato violado: registre, alerte, mas devolva 200
    // para o parceiro nao reenviar em loop por erro de forma
    logger.error('payload fora do contrato', {
      issues: parsed.error.issues,
    });
    metrics.increment('webhook.contract_violation');
    return res.sendStatus(200);
  }

  enqueue(parsed.data); // so dado valido entra na fila
  return res.sendStatus(200);
});

O ponto fino: violação de contrato é diferente de erro de processamento. Devolva 200 para o parceiro não entrar em loop de reenvio, mas dispare métrica e alerta. Um pico em webhook.contract_violation logo após uma mudança do parceiro é o sinal mais cedo possível de que o payload mudou.

04

Snapshot e contrato de APIs externas que você consome

Validar o que entra resolve metade. A outra metade é o que você chama: quando você consome uma API externa, a resposta também tem contrato, e ele também muda. A tática é gravar a resposta esperada uma vez e comparar contra ela para detectar drift.

  • Grave um snapshot da resposta real do parceiro em ambiente controlado, mascarando dados sensíveis e timestamps voláteis.
  • Em cada execução, valide a resposta atual contra o schema derivado do snapshot, não byte a byte, mas estrutura, tipos e campos obrigatórios.
  • Trate campo novo como drift informativo (loga e segue) e campo removido ou tipo trocado como drift que quebra (falha o teste).
  • Rode esses testes contra um sandbox ou contrato gravado, nunca contra produção do parceiro, para não depender da disponibilidade dele no CI.
  • Versione os snapshots no repositório: o diff do snapshot no pull request vira documentação viva de como a API do parceiro evoluiu.
// contractDrift.test.js
const { z } = require('zod');
const snapshot = require('./snapshots/order-response.json');

// schema derivado da resposta gravada
const orderSchema = z.object({
  id: z.string(),
  status: z.enum(['pending', 'paid', 'failed']),
  amount: z.number(),
  currency: z.string().length(3),
});

test('resposta do parceiro continua dentro do contrato', async () => {
  // em CI: usa o snapshot; em job dedicado: bate no sandbox
  const response = process.env.HIT_SANDBOX
    ? await fetchFromSandbox()
    : snapshot;

  const result = orderSchema.safeParse(response);
  expect(result.success).toBe(true);
});

05

Versionamento e estratégia de migração

Parceiros sérios versionam. A Graph API da Meta, por exemplo, expõe a versão na URL (/v21.0/) e deprecia versões antigas em janela conhecida. Tratar versão como detalhe escondido no código é receita para quebra na data de sunset. A migração tem que ser um processo, não um susto.

  1. Fixe a versão explicitamente em um único lugar (variável de ambiente ou config), nunca espalhada por chamadas soltas no código.
  2. Mantenha um teste de contrato por versão ativa, para que a nova versão seja validada lado a lado com a atual antes de virar a chave.
  3. Quando o parceiro anunciar uma versão nova, suba um job que roda os contratos contra ela e mostra o diff de payload em relação à versão em uso.
  4. Faça a migração em canary: aponte uma fração do tráfego para a versão nova, observe métricas e contrato, e só então promova para 100%.
  5. Acompanhe a data de depreciação da versão antiga como item de backlog com prazo, e deixe o teste de contrato falhar de propósito quando faltar pouco para o sunset.
Parceiro anuncia v(N+1)
        |
        v
Contrato v(N) [atual] ---+
                         |--> diff de payload --> revisão
Contrato v(N+1) [novo] --+
        |
        v
Canary 5% --> metricas ok? --> promove 100% --> deprecia v(N)

06

Onde rodar: CI e antes do deploy

Teste de contrato só reduz regressão se rodar no momento certo. Ele vive em dois lugares: no CI de todo pull request, validando schema de webhook e snapshots gravados sem depender do parceiro, e em um job agendado que bate no sandbox do parceiro para pegar mudanças que ainda não chegaram ao seu código.

  • No CI de pull request: valide schemas de webhook e snapshots gravados; rápido, determinístico e sem rede externa.
  • Antes do deploy: o pipeline deve barrar a promoção se algum contrato estiver vermelho, tratando contrato quebrado como build quebrado.
  • Em job agendado (nightly): bata no sandbox do parceiro para detectar drift que o snapshot ainda não capturou, e abra alerta quando divergir.
  • Em produção: a validação do payload recebido continua ligada como última linha, emitindo métrica de violação de contrato em tempo real.
# .github/workflows/contract.yml
name: contract-tests
on:
  pull_request:
  schedule:
    - cron: '0 6 * * *' # nightly contra o sandbox

jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      # PR: snapshots e schemas, sem rede externa
      - run: npm run test:contract
      # nightly: bate no sandbox do parceiro
      - if: github.event_name == 'schedule'
        run: npm run test:contract:sandbox
        env:
          HIT_SANDBOX: 'true'

FAQ

Perguntas frequentes

Teste de contrato substitui meus testes de integração?

Não, eles cobrem coisas diferentes. O teste de integração verifica se o seu fluxo funciona fim a fim com um payload válido. O teste de contrato verifica se o payload do parceiro continua sendo o que você assumiu. Você pode ter integração verde e contrato vermelho ao mesmo tempo: é exatamente esse o caso da quebra silenciosa que o contrato pega.

Devo usar Pact ou só validação de schema com zod?

Depende de quem controla o contrato. Pact brilha em arquitetura interna de microserviços, onde você controla consumidor e provider e quer evitar quebrar clientes. Para um parceiro externo que você só consome, como a Cloud API da Meta, validação de schema com zod mais snapshots gravados costuma ser mais simples e direta, porque você não tem como impor um contrato ao provider, apenas afirmar o que espera dele.

Como evito que o teste de contrato fique instável por bater na API real?

Separe os dois modos. No CI de pull request, rode só contra snapshots gravados e schemas, sem rede externa, para ter um teste rápido e determinístico que nunca falha por o parceiro estar fora do ar. Deixe a chamada ao sandbox real em um job agendado à parte, cuja falha vira alerta de drift e não bloqueia o deploy de quem não mexeu na integração.

Transforme a quebra silenciosa em teste vermelho

A diferença entre descobrir uma mudança de payload no CI ou pelo cliente reclamando é ter o contrato escrito e executável. Valide todo webhook recebido contra um schema, grave snapshots das respostas que você consome para detectar drift e trate versão do parceiro como processo de migração com canary. Rode tudo no CI e antes do deploy. Posso ajudar a montar essa camada de QA de contrato na sua integração.