Testando um recurso de ponta a ponta

Criar, ler, atualizar e apagar, e conferir que cada operação realmente aconteceu.

A maioria das APIs é construída em torno de recursos: livros, pedidos, usuários, faturas. Para cada recurso costuma haver um jeito de criar, ler, alterar e apagar. Quem testa chama isso de CRUD (create, read, update, delete), e um teste CRUD acompanha um registro pela vida inteira. É a primeira coisa a testar em qualquer recurso novo, porque quase todas as outras funcionalidades dependem disso funcionar.

Esta lição usa /api/Booksentre para ver. Ele aceita todas as operações, e a sua página de referência lista os campos. Você já conhece os métodos de Métodos HTTP. Aqui a pergunta é outra: como provar que cada operação fez o que disse?

Uma resposta é uma alegação, não uma prova

Quando um servidor responde a um POST com 201 e uma cópia do registro, ele está dizendo o que acha que guardou. Essa cópia geralmente é montada a partir do mesmo objeto que estava prestes a ser salvo, então pode parecer perfeita mesmo quando o salvamento falhou, ou quando um campo se perdeu no caminho até o banco de dados. O único jeito de saber o que foi guardado é pedir de novo numa requisição separada.

Então um teste CRUD alterna entre fazer algo e conferir com uma leitura nova:

  1. Crie um registro. Confira a resposta.
  2. Leia-o pelo ID. Compare cada campo com o que você enviou.
  3. Encontre-o no endpoint de listagem.
  4. Substitua-o com PUT. Leia de novo.
  5. Altere um campo com PATCH. Leia de novo.
  6. Apague-o. Leia de novo e espere um 404.
  7. Apague de novo e veja o que acontece.

Criar

Terminal
curl -i -X POST https://api.testingapis.com/api/Books \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Testing Web APIs",
    "author": "Mark Winteringham",
    "isbn": "9781617299537",
    "genre": "testing",
    "publishedYear": 2022,
    "pages": 264,
    "language": "English",
    "price": 49.99,
    "rating": 4.6,
    "available": true,
    "tags": ["api", "testing"]
  }'
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita
Resposta
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
  "message": "Book created successfully",
  "data": {
    "id": 26,
    "title": "Testing Web APIs",
    "author": "Mark Winteringham",
    "isbn": "9781617299537",
    "genre": "testing",
    "publishedYear": 2022,
    "pages": 264,
    "language": "English",
    "price": 49.99,
    "rating": 4.6,
    "available": true,
    "tags": ["api", "testing"]
  }
}

O que conferir nesta resposta, mais ou menos por ordem de importância:

  • O status é 201, não 200. Os dois significam sucesso, mas 201 diz que existe um recurso novo, e os clientes às vezes tomam decisões com base nisso.
  • Há um id, e ele é um número (ou o tipo que a documentação prometer). Guarde-o. Todos os passos seguintes precisam dele.
  • Cada campo que você enviou volta com o mesmo valor. Compare um por um, inclusive os que parecem sem graça. Um preço de 49.99 que volta como 49.9899999 ou "49.99" é um bug.
  • Se um cabeçalho Location aponta para o registro novo. Muitas APIs REST mandam um com o 201. Esta não manda. É uma escolha de projeto, não um defeito, mas você precisa saber qual é o caso da sua API e colocar isso nos testes.

Agora veja o que acontece com um pequeno erro no corpo. O campo se chama publishedYear, e aqui é enviado como year:

Terminal
curl -X POST https://api.testingapis.com/api/Books \
  -H "Content-Type: application/json" \
  -d '{"title": "The Test Book", "author": "Ada Tester", "year": 2026}'
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita
Resposta (201 Created)
{
  "message": "Book created successfully",
  "data": {
    "id": 26,
    "title": "The Test Book",
    "author": "Ada Tester",
    "isbn": "",
    "genre": "",
    "publishedYear": 0,
    "pages": 0,
    "language": "",
    "price": 0,
    "rating": 0,
    "available": false,
    "tags": []
  }
}

O servidor aceitou a requisição, ignorou year sem dizer nada e guardou 0como ano. Nada no código de status diz que algo deu errado. Você só pega isso comparando o registro guardado com o que pretendia enviar. Campos ignorados em silêncio são uma das fontes mais comuns de bugs do tipo "os dados estão errados e ninguém sabe por quê". Algumas APIs recusam campos desconhecidos; a próxima lição trata disso.

Campos que o servidor preenche

Alguns campos não vêm de você: o ID e, em muitos recursos, timestamps como createdAt ou updatedAt. Você não pode compará-los com um valor fixo, porque mudam a cada execução. Teste as propriedades deles:

  • O ID está presente, tem o tipo certo e é diferente dos IDs que você criou antes.
  • Um createdAt é uma data válida no formato documentado e fica a poucos segundos de quando você enviou a requisição. Leve em conta diferenças de relógio entre a sua máquina e o servidor.
  • Um updatedAt muda depois de uma atualização, e o createdAt não.
  • Mandar o seu próprio id no corpo não deixa você escolhê-lo. A documentação de Books diz que um ID no corpo é ignorado. Experimente com "id": 1 e confira depois que o livro 1 continua intacto.

Ler de novo

Busque o registro pelo ID que você recebeu e compare o corpo inteiro com o que enviou. Depois confira a listagem. Nesta API dá para buscar pelo título:

GET/api/Books?q=web%20apis
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita

Se o seu livro não estiver nos resultados, a resposta de criação mentiu ou a listagem lê de um lugar diferente do endpoint de registro único (um cache, um índice de busca, uma réplica de leitura). Tudo isso acontece em sistemas reais. Conferir a listagem também pega um problema mais discreto: o registro foi salvo mas fica de fora das listas, por exemplo porque um filtro padrão esconde registros com algum campo vazio.

Atualizar com PUT e PATCH

PUT substitui o registro inteiro. O que você deixar de fora é zerado, geralmente para um valor vazio ou padrão. As pessoas esquecem disso o tempo todo, no cliente e no servidor, então teste de propósito. Aqui um livro é atualizado com só três campos:

Terminal
curl -X PUT https://api.testingapis.com/api/Books/27 \
  -H "Content-Type: application/json" \
  -d '{"title": "Lessons Learned in Software Testing", "author": "Cem Kaner", "price": 39.0}'
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita
Resposta (200 OK)
{
  "message": "Book updated successfully",
  "data": {
    "id": 27,
    "title": "Lessons Learned in Software Testing",
    "author": "Cem Kaner",
    "isbn": "",
    "genre": "",
    "publishedYear": 0,
    "pages": 0,
    "language": "",
    "price": 39.0,
    "rating": 0,
    "available": false,
    "tags": []
  }
}

O ISBN, o número de páginas, o ano e as tags sumiram. Esse é o comportamento correto do PUT. Se o servidor tivesse mantido os valores antigos, estaria agindo como PATCH, e um cliente que dependesse do PUT para limpar um campo nunca conseguiria. A mesma requisição também mandou "id": 1 no corpo; o registro manteve o ID 27, e um GET separado confirmou que o livro 1 não mudou.

Um PUT sem um campo obrigatório falha com 400, como deve: só {"price": 35} devolve Missing required field: title.

PATCH altera só os campos que você envia. Esta API usa JSON merge patch, e a resposta lista o que mudou e o que ignorou:

Terminal
curl -X PATCH https://api.testingapis.com/api/Books/26 \
  -H "Content-Type: application/merge-patch+json" \
  -d '{"price": 35, "available": false, "colour": "blue", "id": 999}'
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita
Resposta (200 OK)
{
  "message": "Book patched successfully",
  "changed": ["price", "available"],
  "ignored": ["colour", "id"],
  "data": {
    "id": 26,
    "title": "Testing Web APIs",
    "author": "Mark Winteringham",
    "isbn": "9781617299537",
    "genre": "testing",
    "publishedYear": 2022,
    "pages": 264,
    "language": "English",
    "price": 35,
    "rating": 4.6,
    "available": false,
    "tags": ["api", "testing"]
  }
}

Confira se os dois campos citados mudaram, se todos os outros mantiveram o valor antigo e se o id ignorado foi mesmo ignorado. Depois faça um GET novo e confira tudo de novo. Um PATCH que atualiza a resposta mas não o registro guardado é raro, mas quando acontece é exatamente o tipo de bug que chega à produção, porque o cliente mostra o valor novo até a página ser recarregada.

Apagar, e apagar de novo

Terminal
$ curl -i -X DELETE https://api.testingapis.com/api/Books/26
HTTP/1.1 200 OK

{"message":"Book deleted successfully"}

$ curl -i https://api.testingapis.com/api/Books/26
HTTP/1.1 404 Not Found

{"status":404,"error":"Not Found","message":"Book with ID 26 does not exist."}

$ curl -i -X DELETE https://api.testingapis.com/api/Books/26
HTTP/1.1 404 Not Found

{"status":404,"error":"Not Found","message":"Book with ID 26 does not exist."}
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita

Depois de apagar, um GET precisa devolver 404. Confira também se o registro sumiu da listagem e se a contagem diminuiu (/api/Books/countentre para ver devolve {"count": 25} com os dados iniciais).

O segundo DELETE é onde as APIs diferem. Esta devolve 404. Outras devolvem 204 ou 200 em toda exclusão, com o argumento de que o estado final (esse registro não existe) é o mesmo. As duas opções se defendem. O que você procura é um 500, ou um 200 dizendo que apagou algo que não estava lá. Mande também PUT e PATCH para o ID apagado. Aqui o PATCH recebe 404, mas um PUT com corpo incompleto recebe 400 pelo campo que falta antes de o servidor perceber que o registro não existe. Qual erro vence quando dois se aplicam merece uma frase na documentação, e muitas vezes não tem nenhuma.

Crie os seus próprios dados

Dá vontade de testar contra o registro 1 porque ele sempre está lá. Não faça isso. Qualquer pessoa pode alterar ou apagar o livro 1 num servidor compartilhado, e um teste que o edita quebra todos os outros testes que o leem. Cada teste deve criar os registros de que precisa, trabalhar só com os IDs que recebeu e apagá-los no fim.

Esta API é um bom lugar para ver o porquê. Ela guarda os dados em memória e é compartilhada por todo mundo que a usa. Enquanto esta lição era escrita, outro usuário criou um livro com ID 26 nos poucos segundos entre uma das nossas exclusões e a criação seguinte. Um teste que presumisse que "o próximo livro será o 26" teria falhado por motivos que não tinham nada a ver com o código testado.

O mesmo experimento mostrou outra coisa. Depois que o livro 26 foi apagado, o próximo livro criado recebeu o ID 26 de novo. Este servidor distribui o próximo número livre, então os IDs são reutilizados. Se outro sistema guardou "livro 26" antes da exclusão, agora ele aponta para outro livro. Se isso é um bug depende do produto, mas é o tipo de achado que vale levantar. É por isso que a maioria dos bancos de dados nunca reutiliza IDs.

Endpoints em lote

Muitas APIs deixam criar ou apagar vários registros numa requisição. A pergunta interessante é o que acontece quando alguns itens são ruins.

Terminal
curl -X POST https://api.testingapis.com/api/Books/bulk \
  -H "Content-Type: application/json" \
  -d '[
    {"title": "Bulk A", "author": "Tester"},
    {"title": "Bulk B", "author": "Tester"},
    {"title": "", "author": "Tester"}
  ]'

{"status":400,"error":"Bad Request","message":"Item [2]: Missing required field: title"}
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita

O terceiro item tem título vazio, então a requisição inteira falha e o erro cita o item pelo índice. A documentação de Books diz que nada é guardado nesse caso, e você deve verificar: confira se "Bulk A" não existe depois da requisição que falhou. Uma API que salva os dois primeiros itens e depois falha deixa o cliente sem saber o que repetir.

A exclusão em lote informa quais IDs encontrou e quais não:

Terminal
curl -X DELETE "https://api.testingapis.com/api/Books/bulk?ids=27,28,5000"

{"message":"2 book record(s) deleted","deleted":[27,28],"notFound":[5000]}
Este exemplo usa endpoints para membros conectados.EntrarCriar uma conta gratuita
Caso em loteO que conferir
Todos os itens válidosCada item é guardado, os IDs são distintos, a contagem sobe no número enviado.
Um item inválidoNada é guardado, ou a resposta diz exatamente quais itens foram guardados.
Array vazioUm 400 claro, não um 201 que não criou nada.
Acima do limite (100 aqui)Um 400 que informe o limite.
IDs duplicados numa exclusão em loteCada registro apagado uma vez, sem erro na segunda ocorrência.

Exercício

Rode o ciclo de vida completo em /api/Moviesentre para ver, usando a página de referência para os campos. Crie um filme com todos os campos preenchidos e então:

  1. Leia de novo e compare cada campo com o que você enviou.
  2. Encontre-o com ?q= e uma palavra do título.
  3. Faça PUT só com os campos obrigatórios. Quais campos foram zerados?
  4. Faça PATCH de um campo e mande um nome de campo que não existe. Leia a lista ignored.
  5. Apague-o, depois faça GET, PATCH e DELETE de novo. Anote cada status.

Termine anotando uma coisa que você perguntaria aos desenvolvedores (um campo ignorado, um ID reutilizado, um erro que prevaleceu sobre outro). Decidir o que reportar faz tanto parte do teste quanto as requisições.

Pratique e vá além