Swagger não é o contrato: teste se a API faz o que a especificação promete
Use o Swagger para explorar, mas trate a especificação OpenAPI como contrato: versione, compare a cada mudança e teste no CI se respostas, erros e autenticação batem com ela.
A integração com o fornecedor funcionou na semana de testes. Em produção, o campo valorTotal veio como texto em vez de número, a página 2 repetiu itens da página 1 e a rota que o Swagger mostrava com cadeado respondeu sem token. Tudo isso com a página do Swagger exibindo o exemplo bonito de sempre. A resposta curta: o Swagger UI desenha a especificação OpenAPI, não o comportamento do servidor. O contrato só vale quando é testado contra a API real, a cada mudança, nas respostas de sucesso e de erro.
O exemplo deste guia é a API de pedidos de uma distribuidora, em ASP.NET Core, consumida por um app e por dois parceiros. Na última seção está o outro lado: o que fazer quando você é quem consome a API de alguém, como a API pública de consulta do PNCP que inspirou o post original.
Por que a página e a API se separam
A especificação é gerada a partir de anotações, tipos e convenções do código, ou escrita à mão num arquivo YAML. Nos dois casos, ela descreve o que alguém declarou. O servidor faz o que o código faz. Os dois se afastam de jeitos previsíveis:
- Erros não declarados. A rota documenta 200 e 400; na prática devolve 500 com página HTML quando o banco cai, ou 404 em vez de 403.
- Tipos frouxos. Um campo declarado como número sai como texto porque passou por um
ToString()num mapeamento. - Autenticação descrita, não aplicada. O cadeado no Swagger vem de uma configuração global; a rota esqueceu o atributo de autorização.
- Mudança sem aviso. Alguém renomeia um campo, a especificação muda junto, e ninguém percebe que isso quebrou os parceiros.
- Exemplos irreais. O exemplo mostra uma lista com um item, sem paginação, sem campos nulos, sem acento.
Como transformar a especificação em contrato
- Versione o arquivo da especificação no repositório. Exporte o JSON no build e faça commit. Assim, toda mudança de contrato aparece no diff do pull request.
- Compare a especificação nova com a anterior no CI com uma ferramenta de diff de OpenAPI, como o
oasdiff, e trate mudança incompatível como decisão explícita. - Teste a API real contra a especificação com uma ferramenta que gera requisições a partir dela, como o Schemathesis, num ambiente de teste.
- Escreva testes para o que a especificação não expressa bem: autenticação, autorização entre clientes, paginação e formato de erro.
- Declare as respostas de erro na especificação, com um formato único (o
application/problem+jsondo ASP.NET Core serve).
No .NET 9 em diante, o pacote Microsoft.AspNetCore.OpenApi gera o documento com builder.Services.AddOpenApi() e app.MapOpenApi(), servido por padrão em /openapi/v1.json. Projetos com Swashbuckle costumam servir em /swagger/v1/swagger.json. O fluxo no CI fica assim:
# baixar a especificação do build atual e comparar com a da branch principal
curl -s http://localhost:5000/openapi/v1.json -o openapi-nova.json
git show origin/main:contrato/openapi.json > openapi-main.json
oasdiff breaking openapi-main.json openapi-nova.json
# gerar requisições a partir da especificação e conferir as respostas
schemathesis run http://localhost:5000/openapi/v1.json
O oasdiff breaking lista mudanças que quebram clientes: campo obrigatório novo na entrada, campo removido da saída, tipo alterado. Configure o passo para falhar o build conforme a documentação da versão que você usa. O Schemathesis manda variações de entrada que você não pensaria em testar e acusa respostas fora do declarado, como um 500 ou um corpo que não bate com o schema.
Testes que a especificação não faz por você
Ferramentas genéricas não sabem que o cliente A não pode ver o pedido do cliente B. Esses testes você escreve, e eles rodam no mesmo CI:
# test_contrato_pedidos.py
import os
import requests
BASE = os.environ.get("API_BASE", "http://localhost:5000")
TOKEN_A = os.environ["TOKEN_CLIENTE_A"]
PEDIDO_DE_B = os.environ["PEDIDO_DO_CLIENTE_B"]
def cabecalho(token):
return {"Authorization": "Bearer " + token}
def test_sem_token_recebe_401():
r = requests.get(BASE + "/pedidos", timeout=10)
assert r.status_code == 401
def test_lista_traz_campos_e_tipos_do_contrato():
r = requests.get(BASE + "/pedidos?pagina=1&tamanho=50",
headers=cabecalho(TOKEN_A), timeout=10)
assert r.status_code == 200
corpo = r.json()
assert isinstance(corpo["total"], int)
for pedido in corpo["itens"]:
assert isinstance(pedido["valorTotalCentavos"], int)
assert pedido["status"] in ("aberto", "faturado", "cancelado")
def test_pedido_de_outro_cliente_nao_vaza():
r = requests.get(BASE + "/pedidos/" + PEDIDO_DE_B,
headers=cabecalho(TOKEN_A), timeout=10)
assert r.status_code in (403, 404)
def test_erro_tem_formato_unico():
r = requests.get(BASE + "/pedidos?pagina=-1",
headers=cabecalho(TOKEN_A), timeout=10)
assert r.status_code == 400
assert r.headers["Content-Type"].startswith("application/problem+json")
Os valores em centavos não são enfeite: dinheiro como inteiro evita o erro de arredondamento descrito em dinheiro em centavos. E se a paginação repete itens, o problema costuma ser a ordenação, como em paginação que não pula.
Quando você consome a API de outro
Do lado de quem integra, o Swagger é o ponto de partida. Experimente as rotas no ambiente de teste, com dado fictício. Depois, grave respostas reais e compare com o que a página promete: campos nulos, datas em formato diferente, paginação. Quando divergirem, vale o que a API devolveu, e o seu código deve ser tolerante a campos extras e rígido nos campos que você usa. Se a API tem também um manual em PDF com versão e data, ele costuma ser a referência formal, como discute PDF de integração tem versão.
Erros comuns
- Tratar o Swagger UI como documentação completa. Erros, limites e regras de negócio ficam de fora, e o parceiro descobre em produção.
- Especificação fora do repositório. Ninguém vê no pull request que um campo foi removido, e o app de alguém quebra na segunda.
- Só testar o caminho feliz. O 500 com HTML no lugar do JSON de erro derruba o parser do cliente.
- Confiar no cadeado da página. O ícone vem da configuração da documentação; a proteção vem da rota. Teste sem token.
- "Try it out" com token real em produção. O botão executa de verdade: um DELETE de teste apaga um registro de verdade.
- Ambiente de teste com cópia de dados reais. A página de experimentação vira um jeito fácil de expor dado pessoal.
Checklist
- Arquivo da especificação versionado e atualizado pelo build.
- Diff de especificação no CI, com mudança incompatível barrada ou aprovada de forma explícita.
- Teste gerado a partir da especificação rodando contra o ambiente de teste.
- Testes de 401 sem token e de acesso a recurso de outro cliente.
- Respostas de erro declaradas e com formato único.
- Exemplos com paginação, campos nulos e acentos.
- Decisão registrada sobre expor ou não o Swagger UI em produção.
Escolha a rota mais usada da sua API e chame sem token pelo terminal. Ela responde 401, ou devolve dados? E a pergunta que mede o quanto a especificação é contrato: se alguém remover um campo da resposta hoje, qual teste falha antes do deploy?
Referência: https://pncp.gov.br/api/consulta/swagger-ui/index.html