Programação Por Equipe DevBras

Capa do artigo: Swagger não é o contrato: teste se a API faz o que a especificação promete

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

  1. 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.
  2. 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.
  3. 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.
  4. Escreva testes para o que a especificação não expressa bem: autenticação, autorização entre clientes, paginação e formato de erro.
  5. Declare as respostas de erro na especificação, com um formato único (o application/problem+json do 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?

Avalie este artigo

Referência: https://pncp.gov.br/api/consulta/swagger-ui/index.html