Testando validação e erros

Entradas ruins, campos ausentes, tipos errados e como é um bom erro.

A lição anterior enviou dados bons e conferiu se foram guardados. Esta envia dados ruins e confere se são recusados. Na maioria dos projetos, é aqui que estão os bugs. Os desenvolvedores escrevem o caminho feliz primeiro, testam à mão enquanto constroem e depois ficam sem tempo antes de pensar no que mais um cliente poderia enviar.

Um teste de validação tem duas partes. O servidor precisa recusar a requisição com o status certo, e não pode guardar nada. Depois disso, importa a qualidade da mensagem de erro, porque em algum momento outro desenvolvedor vai lê-la tentando descobrir o que fez de errado.

Um formulário com regras

POST /api/validation/registerentre para ver é um endpoint de cadastro com regras escritas. A página de referência lista todas: o nome de usuário tem de 3 a 20 letras, dígitos ou sublinhados, a idade é um inteiro de 18 a 120, o país é um de nove códigos, e assim por diante. Nada é guardado, então você pode enviar o mesmo corpo quantas vezes quiser. Uma requisição válida:

POST/api/validation/register
{
  "username": "ava_t",
  "email": "ava@example.com",
  "password": "Str0ng!Pass",
  "confirmPassword": "Str0ng!Pass",
  "age": 29,
  "country": "us",
  "phone": "+15550101",
  "acceptTerms": true
}
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita
Resposta
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
  "message": "User registered successfully",
  "data": {
    "id": 1001,
    "username": "ava_t",
    "email": "ava@example.com",
    "age": 29,
    "country": "US",
    "createdAt": "2026-09-30T18:32:12Z"
  }
}

Repare que "us" voltou como "US". O servidor normalizou o valor em vez de recusá-lo. Tudo bem, mas é um comportamento, e um teste deve fixá-lo para que ninguém o mude sem querer.

Agora quebre quase todas as regras de uma vez:

POST/api/validation/register
{
  "username": "ab",
  "email": "ava@example",
  "password": "password",
  "confirmPassword": "Password",
  "age": 17,
  "country": "XX",
  "acceptTerms": false
}
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita
Resposta
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://example.com/problems/validation-error",
  "title": "One or more fields failed validation.",
  "status": 422,
  "detail": "7 field(s) failed validation.",
  "instance": "/api/validation/register",
  "errors": {
    "username": ["Username must be 3-20 characters long."],
    "email": ["Email must be a valid email address."],
    "password": [
      "Password must contain an uppercase letter.",
      "Password must contain a digit.",
      "Password must contain a special character."
    ],
    "confirmPassword": ["Passwords do not match."],
    "age": ["Age must be between 18 and 120."],
    "country": ["Country must be one of: US, GB, IN, DE, FR, JP, CA, AU, BR."],
    "acceptTerms": ["You must accept the terms and conditions."]
  }
}
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita

Esta é uma boa resposta de erro, e vale a pena ver por quê, para reconhecer uma ruim depois.

  • Ela reporta todos os problemas de uma vez. Um cliente corrigindo o formulário não precisa enviá-lo sete vezes para achar sete erros.
  • Cada erro é indexado pelo nome do campo que o cliente enviou, então uma interface pode mostrá-lo ao lado do campo certo.
  • As mensagens dizem a regra ("between 18 and 120") em vez de dizer que o valor está errado e deixar o cliente adivinhar por quê.
  • Ela segue a RFC 7807 (atualizada como RFC 9457), um formato padrão para erros HTTP com os campos type, title, status, detail e instance, e o tipo de mídia application/problem+json. Você não precisa exigir esse formato, mas se uma API diz usá-lo, confira se os campos estão lá e se o status no corpo bate com o código de status real.

O que enviar

Você não vai testar tudo em todo campo. Passe por cada campo e pergunte quais destes poderiam se aplicar:

Tipo de entradaExemplos
AusenteDeixe o campo de fora. Envie como null. Envie uma string vazia. Eles podem se comportar de jeitos diferentes.
Tipo errado"29" para um número, 29 para uma string, true para uma string, um objeto onde se espera uma lista
LimitesO mínimo e o máximo exatos, um de cada lado, zero, números negativos, um decimal onde se espera um inteiro
TamanhoVazio, um caractere, o tamanho máximo, um a mais, e algo bem longo (10.000 caracteres)
FormatoE-mails, datas, telefones, URLs e códigos quase certos
Espaços e maiúsculasEspaços no início, um título só de espaços, Admin quando admin já está em uso
Campos desconhecidosUm nome de campo errado, um campo a mais, um campo que o cliente não deveria poder definir
O próprio corpoSem corpo, JSON malformado, um array em vez de um objeto, o Content-Type errado

Limites no endpoint de cadastro

A regra de idade é de 18 a 120, então os valores interessantes são 17, 18, 120 e 121. Nesta API, 18 e 120 são aceitos, e 17 e 121 recebem "Age must be between 18 and 120." Um valor de 18.5recebe outra mensagem, "Age must be an integer.", e a string "29" também. Algumas APIs convertem "29" em 29 em silêncio. Nenhum dos dois comportamentos está errado em si, mas os clientes vão passar a depender daquele que for para a produção.

O nome de usuário tem de 3 a 20 caracteres: abc e um nome de 20 letras passam, um de 21 letras falha. Um nome com espaço é recusado com uma mensagem sobre os caracteres permitidos. O nome já em uso admin é recusado, e Admin também, o que mostra que a verificação de unicidade ignora maiúsculas. Se não ignorasse, dois usuários poderiam acabar com nomes que parecem iguais na maioria dos lugares.

A verificação de e-mail é a bagunça de sempre. ava@example, ava@@example.com e ava.example.com são recusados, e " ava@example.com", com um espaço no início, também. ava+qa@example.com e AVA@EXAMPLE.COM são aceitos, o que está certo. a@b.c também é aceito. A validação real de e-mail é uma discussão longa que ninguém vence, então o teste é menos sobre a regra perfeita e mais sobre os casos que importam para o seu produto: endereços com sinal de mais, maiúsculas e se os espaços em volta são removidos ou recusados.

Vazio não é o mesmo que ausente

Envie {} e todo campo obrigatório é reportado como "is required". Envie os mesmos campos com valores null e você recebe o mesmo resultado. É consistente, o que é bom. Em /api/Booksentre para ver, um título de três espaços também é tratado como ausente. Muitas APIs só conferem se uma string existe, então um título só de espaços, ou uma string vazia, passa direto e depois aparece como uma linha em branco na tabela de alguém.

Limites de tamanho muitas vezes simplesmente não existem. /api/Booksentre para ver aceitou um título de 5.000 caracteres com 201. Não há limite documentado, então pode ser proposital, mas é o tipo de coisa com que uma coluna do banco de dados ou o layout de uma interface vão discordar mais tarde. Pergunte qual deveria ser o limite e teste um caractere de cada lado.

Campos desconhecidos

Quando um cliente envia um campo que o servidor não conhece, o servidor pode ignorá-lo ou recusá-lo. Na lição de CRUD, /api/Booksentre para ver ignorou um campo year escrito errado e guardou o ano 0. /api/validation/strictentre para ver recusa:

Terminal
curl -X POST https://api.testingapis.com/api/validation/strict \
  -H "Content-Type: application/json" \
  -d '{"name": "Widget", "email": "a@b.co", "quantity": 2, "discount": 50}'

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://example.com/problems/unknown-fields",
  "title": "Request body contains unknown fields.",
  "status": 400,
  "detail": "Allowed fields: name, email, quantity (names are case-sensitive).",
  "instance": "/api/validation/strict",
  "unknownFields": ["discount"]
}
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita

Aqui os nomes de campo diferenciam maiúsculas de minúsculas, então Name em vez de name também é um campo desconhecido. Recusar é mais seguro, já que os erros de digitação aparecem na hora. Ignorar é mais amigável quando uma API evolui e clientes antigos mandam campos que foram removidos. O seu trabalho é descobrir qual abordagem a API usa e se ela é usada de forma consistente entre os endpoints.

Há um tipo de campo desconhecido que é questão de segurança e não de estilo: um campo que existe no registro mas que o cliente não deveria poder definir, como role, isAdmin, o price de um pedido ou o id. Se o servidor copia todo campo recebido para o registro, um usuário pode se promover sozinho. A lição de segurança chama isso de mass assignment.

Corpos quebrados

Em seguida, teste a requisição como um todo. Estas são respostas reais do endpoint de cadastro:

Respostas
# No body at all
HTTP/1.1 400 Bad Request
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"":["A non-empty request body is required."]},"traceId":"00-fc01be6d..."}

# {"username":"ava_t",}  (trailing comma)
HTTP/1.1 400 Bad Request
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"$":["The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. Path: $ | LineNumber: 0 | BytePositionInLine: 20."]},"traceId":"00-e13ac011..."}

# [] instead of an object
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"status":400,"error":"Bad Request","message":"Request body must be a JSON object."}

# Valid JSON sent as Content-Type: text/plain
HTTP/1.1 415 Unsupported Media Type
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.16","title":"Unsupported Media Type","status":415,"traceId":"00-4cadc1e1..."}

As quatro são tratadas, e o 415 para o Content-Type errado está exatamente certo. Mas olhe os formatos. Três respostas são problem+json com um mapa errors. O caso do array devolve um formato totalmente diferente, {status, error, message}, com um tipo de conteúdo JSON comum. Um cliente que interpreta erros de um jeito vai quebrar com o outro. Isso acontece quando alguns erros vêm do framework e outros do código da aplicação, e é um dos achados mais comuns em teste de API. Reúna os formatos de erro que você vê numa API e coloque-os lado a lado.

O que um erro não deve conter

Aqui está o /api/Booksentre para ver recebendo uma string onde espera um número:

Terminal
curl -X POST https://api.testingapis.com/api/Books \
  -H "Content-Type: application/json" \
  -d '{"title": "x", "author": "y", "pages": "many"}'

HTTP/1.1 400 Bad Request

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "item": ["The item field is required."],
    "$.pages": ["The JSON value could not be converted to System.Int32. Path: $.pages | LineNumber: 0 | BytePositionInLine: 40."]
  },
  "traceId": "00-d027cc55a2925746a1bf93cc96d2306d-02234fa55d940604-00"
}
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita

O status está certo. O conteúdo tem dois problemas. A mensagem cita System.Int32, o que conta a um estranho que o servidor roda .NET e deixa o cliente com um nome de tipo em vez de "pages precisa ser um número inteiro". E o erro extra "The item field is required." se refere a um nome de parâmetro interno, não a algo que o cliente enviou. Nenhum dos dois é perigoso aqui, mas num produto real você reportaria os dois.

O que nunca pode aparecer num erro: stack traces, trechos de SQL, caminhos de arquivo, nomes de hosts internos e valores de segredos. Já um traceId é útil: ele permite que o suporte encontre o log correspondente no servidor sem expor nada.

Qual código de status

As equipes discutem 400 versus 422. Uma divisão comum é 400 para um corpo que o servidor não conseguiu ler (JSON malformado, tipos errados) e 422 para um corpo legível que quebra uma regra (idade 17). /api/validation/registerentre para ver devolve 400 só quando não consegue interpretar o corpo de jeito nenhum, e 422 para todo o resto, inclusive uma string onde deveria estar a idade. /api/validation/annotationsentre para ver usa 400 para tudo. Qualquer abordagem é aceitável se for consistente e documentada, e dois endpoints da mesma API fazendo diferente merece uma pergunta.

StatusO que significa para uma entrada ruim
400 Bad RequestA requisição está malformada ou inválida. O cliente precisa mudá-la.
415 Unsupported Media TypeO Content-Type não é um dos que o endpoint aceita.
422 Unprocessable ContentO corpo foi interpretado, mas os valores quebram uma regra.
409 ConflictVálido por si só, mas conflita com o estado atual, como um duplicado. (O endpoint de cadastro usa 422 para um nome de usuário já em uso.)
500 Internal Server ErrorNunca é correto para entrada ruim. O servidor quebrou com algo que deveria ter recusado.

A última linha é a única regra sem exceções. Se qualquer entrada que você enviar produzir um 500, você encontrou um bug, por mais estranha que fosse a entrada. O servidor deveria recusá-la e em vez disso caiu. Você pode ver como é um 500 bem formado em /api/validation/problem/internalentre para ver, e os outros documentos de problema de exemplo estão listados aqui:

GET/api/validation/problem
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita

Exercício

Teste POST /api/validation/annotationsentre para ver. O exemplo válido dele está na página de referência. Ele usa a validação nativa do framework em vez de regras escritas à mão.

  • Encontre os limites de username, age e tags.
  • Experimente três telefones e sites inválidos e dois válidos.
  • Envie um corpo inválido para /annotations e para /register e compare as respostas de erro. Que status cada um usa? Como os nomes de campo em errors estão escritos em comparação com os que você enviou?
  • Envie uma string onde age espera um número. A mensagem vaza alguma coisa?

Descreva as diferenças entre os dois endpoints como se estivesse abrindo um ticket para um desenvolvedor: o que você enviou, o que voltou e o que você esperava.

Pratique e vá além