Toda quebra de API que chegou à produção na minha equipe passou pelo CI. Todas. Os unit tests estavam verdes. A integration suite passou. O deploy saiu, e aí as mensagens no Slack começaram.
O problema não era que não estávamos testando. Estávamos testando a coisa errada. A maioria dos pipelines de CI verifica se o código roda. Eles não verificam se o contract entre producer e consumer ainda está intacto.
Por que seus testes atuais não capturam quebras de API
Unit tests exercitam sua lógica interna. Eles chamam seu handler com dependencies simuladas e fazem asserções sobre a resposta. integration tests sobem toda a stack e batem em endpoints reais.
Ambos testam o producer. Eles verificam que sua API se comporta corretamente quando chamada da forma como você atualmente a chama. Eles não testam se um consumer externo, construído contra o contract da semana passada, ainda consegue falar com você.
As quebras sutis são as perigosas. Você renomeia um campo de user_id para userId porque seu linter reclamou. Você muda uma resposta 200 para retornar um objeto aninhado em vez de um flat. Você torna um parâmetro de query obrigatório que antes era opcional. Seus próprios testes são atualizados no mesmo PR, então tudo passa. Mas todo cliente em produção quebra.
Essa é a diferença entre testar código e testar contracts.
O que conta como uma breaking change de API
Uma breaking change é qualquer modificação que faz um cliente corretamente implementado falhar. Não se trata de bugs. Trata-se da promessa que sua API fez.
As quebras mais comuns caem em três categorias:
- Mudanças estruturais: Remover ou renomear campos, alterar tipos, mudar aninhamento
- Mudanças comportamentais: Tornar parâmetros opcionais obrigatórios, alterar defaults de paginação, modificar formatos de resposta de erro
- Mudanças de ciclo de vida: Remover endpoints, alterar caminhos de URL, descontinuar versões sem aviso
Algumas são óbvias. Outras só são óbvias se você olha para a API da perspectiva do consumer. A maioria das equipes não olha.
Camada 1: Capture quebras estruturais com OpenAPI diffing
Especificações OpenAPI descrevem a forma da sua API. Se você tratar sua spec como um contract, pode compará-la (diff) com a versão anterior e sinalizar breaking changes antes do merge.
Ferramentas como oasdiff comparam dois documentos OpenAPI e categorizam mudanças como breaking, dangerous ou safe. Uma breaking change é algo como remover um campo de resposta ou alterar um parâmetro de opcional para obrigatório.
Veja como fica em um workflow do GitHub Actions:
# .github/workflows/api-contract.yml
name: API Contract Check
on:
pull_request:
paths:
- 'openapi.yaml'
jobs:
diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download oasdiff
run: |
curl -L https://github.com/Tufin/oasdiff/releases/latest/download/oasdiff_linux_amd64.tar.gz | tar xz
sudo mv oasdiff /usr/local/bin/
- name: Check for breaking changes
run: |
oasdiff breaking \
--base origin/main:openapi.yaml \
--revision openapi.yaml \
--fail-on WARN
Isso falha o build se o PR introduzir uma breaking change estrutural. É rápido, deterministic, e captura os renomeamentos e remoções que integration tests deixam passar.
Há uma ressalva. OpenAPI diffing só vê o schema. Ele não consegue dizer se você mudou o significado de um campo mantendo o tipo igual. Um campo booleano active que agora significa “email verificado” em vez de “conta ativada” não aparece no diff. O tipo não mudou. O contract mudou.
Camada 2: Verifique consumer contracts com Pact
Consumer-driven contract testing inverte o modelo. Em vez do provedor de API afirmar sua própria corretude, os consumers definem o que precisam. Essas expectativas se tornam contracts que o provedor deve satisfazer no CI.
Funciona assim. Sua equipe de frontend escreve um teste que diz: “Quando eu chamo GET /users/123, espero um 200 com um body contendo user_id como string.” O Pact registra essa interação e armazena o contract.
Do lado do provedor, seu backend no CI baixa todos os consumer contracts e os reproduz contra o código atual. Se um PR remove user_id ou o altera para número, a verificação do provedor falha. Mesmo que seus próprios testes passem.
Um teste de consumer mínimo com Pact JS:
// consumer.spec.js
const { PactV3 } = require('@pact-foundation/pact');
const { expect } = require('chai');
const provider = new PactV3({
consumer: 'web-app',
provider: 'user-service',
dir: './pacts',
});
describe('GET /users/:id', () => {
it('returns the user', async () => {
await provider
.given('user exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123',
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
user_id: '123',
email: 'alice@example.com',
},
});
await provider.executeTest(async (mockserver) => {
const user = await fetchUser(mockserver.url, '123');
expect(user.user_id).to.equal('123');
});
});
});
A verificação do provedor no CI:
# .github/workflows/verify-contracts.yml
name: Verify Consumer Contracts
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify pacts
run: |
docker run --rm \
-v $(pwd)/pacts:/pacts \
pactfoundation/pact-cli \
verify-provider \
--provider-app-version ${{ github.sha }} \
--pact-broker-base-url https://your-pact-broker.io \
--provider user-service
Isso captura quebras semânticas que schema diffing não consegue ver. Se um consumer depende de um formato de valor específico ou mensagem de erro, o Pact vai sinalizar.
Os trade-offs que a maioria das equipes ignora
Nenhuma abordagem é de graça.
OpenAPI diffing exige que você mantenha uma spec precisa. Se sua spec é gerada a partir de anotações de código, ela permanece atualizada automaticamente. Se é escrita à mão, ela vai divergir, e o diff se torna sem sentido. Specs geradas são melhores para esse workflow.
Pact exige disciplina organizacional. Os consumers devem escrever contract tests. Você precisa de um broker para armazenar e versionar contracts. Quando a expectativa de um consumer está errada, alguém tem que negociar a mudança. Isso cria fricção. Essa é a ideia. A fricção previne quebras silenciosas.
Executar ambos é ideal, mas nem sempre prático. Se você tem um ou dois consumers críticos, o Pact se paga rapidamente. Se você tem dezenas de consumers de API anônimos, OpenAPI diffing é o melhor ponto de partida.
Mais uma coisa: nenhuma das ferramentas captura regressões de performance ou mudanças de autenticação. Um novo endpoint que exige OAuth onde antes não era necessário é uma breaking change. Seu diff tool pode não sinalizar isso se o schema de auth já estava definido em outro lugar. Fique atento às lacunas.
FAQ
O que é uma breaking change de API?
Uma breaking change de API é qualquer modificação em uma API que faz clientes existentes e corretamente implementados falharem. Isso inclui remover campos, alterar tipos, tornar parâmetros opcionais obrigatórios, ou alterar códigos de status de resposta para endpoints existentes.
Como teste de API contract difere de integration test?
integration tests verificam se seu sistema funciona como um todo. Contract tests verificam se a interface pública da API corresponde ao que os consumers esperam. integration tests podem passar mesmo quando o contract quebra, se ambos os lados do sistema são atualizados no mesmo commit.
Posso usar OpenAPI diffing sem escrever specs à mão?
Sim. Ferramentas como oasdiff funcionam com qualquer documento OpenAPI, incluindo aqueles gerados a partir de anotações de código usando bibliotecas como SpringDoc, FastAPI ou drf-spectacular. Specs geradas são frequentemente mais confiáveis porque não podem divergir da implementação.
Preciso de um Pact broker para usar consumer-driven contracts?
Para equipes com mais de alguns serviços, sim. O broker armazena contracts, rastreia versões e mostra quais consumers são afetados por uma mudança proposta. Sem ele, você fica passando arquivos de contract manualmente, o que desmorona rapidamente.
E o GraphQL?
GraphQL tem semânticas diferentes de breaking change. Remover um campo é breaking, mas adicionar um é safe. Ferramentas como GraphQL Inspector fornecem schema diffing similar às ferramentas OpenAPI. O Pact também suporta interações GraphQL.
Comece com o schema diff
Comece com OpenAPI diffing. É o menor esforço e captura as quebras mais comuns. Adicione ao seu CI hoje, mesmo que sua spec seja imperfeita. Uma verificação imperfeita é melhor que nenhuma verificação.
Uma vez que você tenha capturado uma quebra na revisão que teria arruinado o fim de semana de alguém, você vai entender por que isso importa.