Tester une ressource de bout en bout
Créer, lire, mettre à jour et supprimer, et vérifier que chaque opération a vraiment eu lieu.
La plupart des API sont construites autour de ressources : livres, commandes, utilisateurs, factures. Pour chaque ressource, il existe en général un moyen de la créer, de la lire, de la modifier et de la supprimer. On appelle cela CRUD (create, read, update, delete), et un test CRUD suit un enregistrement pendant toute sa vie. C’est la première chose à tester sur toute nouvelle ressource, car presque toutes les autres fonctionnalités en dépendent.
Cette leçon utilise /api/Booksconnectez-vous pour voir. Il accepte toutes les opérations, et sa page de référence liste les champs. Vous connaissez déjà les méthodes grâce aux méthodes HTTP. Ici, la question est différente : comment prouver que chaque opération a fait ce qu’elle prétend ?
Une réponse est une affirmation, pas une preuve
Quand un serveur répond à un POST par un 201 et une copie de l’enregistrement, il vous dit ce qu’il pense avoir stocké. Cette copie est généralement construite à partir du même objet qui allait être enregistré : elle peut donc sembler parfaite même si l’enregistrement a échoué, ou si un champ s’est perdu en route vers la base de données. La seule façon de savoir ce qui a été stocké est de le redemander dans une requête séparée.
Un test CRUD alterne donc entre faire quelque chose et le vérifier par une nouvelle lecture :
- Créez un enregistrement. Vérifiez la réponse.
- Relisez-le par son ID. Comparez chaque champ avec ce que vous avez envoyé.
- Retrouvez-le dans le point d’accès de liste.
- Remplacez-le avec PUT. Relisez-le.
- Modifiez un champ avec PATCH. Relisez-le.
- Supprimez-le. Relisez-le et attendez un 404.
- Supprimez-le à nouveau et regardez ce qui se passe.
Créer
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"]
}'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"]
}
}Ce qu’il faut vérifier sur cette réponse, à peu près par ordre d’importance :
- Le statut est 201, pas 200. Les deux signifient un succès, mais 201 dit qu’une nouvelle ressource existe, et les clients s’en servent parfois pour choisir une branche.
- Il y a un
id, et c’est un nombre (ou le type promis par la documentation). Gardez-le. Toutes les étapes suivantes en ont besoin. - Chaque champ envoyé revient avec la même valeur. Comparez-les un par un, y compris ceux qui semblent anodins. Un prix de
49.99qui revient en49.9899999ou en"49.99"est un bug. - Si un en-tête
Locationpointe vers le nouvel enregistrement. Beaucoup d’API REST en envoient un avec le 201. Celle-ci non. C’est un choix de conception plutôt qu’un défaut, mais vous devez savoir ce que fait votre API et l’inscrire dans vos tests.
Regardez maintenant ce qui se passe avec une petite erreur dans le corps. Le champ s’appelle publishedYear, et il est envoyé ici sous le nom year :
curl -X POST https://api.testingapis.com/api/Books \
-H "Content-Type: application/json" \
-d '{"title": "The Test Book", "author": "Ada Tester", "year": 2026}'{
"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": []
}
}Le serveur a accepté la requête, ignoré year sans un mot et stocké 0comme année. Rien dans le code de statut ne vous dit que quelque chose s’est mal passé. Vous ne le voyez qu’en comparant l’enregistrement stocké avec ce que vous vouliez envoyer. Les champs ignorés en silence sont l’une des sources les plus fréquentes de bugs du type "les données sont fausses et personne ne sait pourquoi". Certaines API refusent au contraire les champs inconnus ; la leçon suivante en parle.
Les champs remplis par le serveur
Certains champs ne viennent pas de vous : l’ID et, sur beaucoup de ressources, des horodatages comme createdAt ou updatedAt. Vous ne pouvez pas les comparer à une valeur fixe, car ils changent à chaque exécution. Testez plutôt leurs propriétés :
- L’ID est présent, a le bon type et diffère des ID que vous avez créés auparavant.
- Un
createdAtest une date valide au format documenté et se situe à quelques secondes du moment où vous avez envoyé la requête. Prévoyez les écarts d’horloge entre votre machine et le serveur. - Un
updatedAtchange après une mise à jour, etcreatedAtnon. - Envoyer votre propre
iddans le corps ne vous permet pas de le choisir. La documentation de Books indique qu’un ID dans le corps est ignoré. Essayez avec"id": 1et vérifiez ensuite que le livre 1 n’a pas été touché.
Le relire
Récupérez l’enregistrement avec l’ID que l’on vous a donné, et comparez tout le corps avec ce que vous avez envoyé. Puis vérifiez la liste. Sur cette API, vous pouvez chercher par titre :
/api/Books?q=web%20apisSi votre livre n’est pas dans les résultats, soit la réponse de création a menti, soit la liste lit à un autre endroit que le point d’accès d’un enregistrement unique (un cache, un index de recherche, un réplica en lecture). Tout cela arrive dans les vrais systèmes. Vérifier la liste attrape aussi un problème plus discret : l’enregistrement est sauvegardé mais filtré hors des listes, par exemple parce qu’un filtre par défaut cache les enregistrements dont un champ est vide.
Mettre à jour avec PUT et PATCH
PUT remplace tout l’enregistrement. Ce que vous omettez est réinitialisé, en général à une valeur vide ou par défaut. On l’oublie tout le temps, côté client comme côté serveur, alors testez-le exprès. Ici, un livre est mis à jour avec seulement trois champs :
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}'{
"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": []
}
}L’ISBN, le nombre de pages, l’année et les étiquettes ont disparu. C’est le comportement correct de PUT. Si le serveur avait gardé les anciennes valeurs, il se comporterait comme PATCH, et un client qui compte sur PUT pour vider un champ n’y arriverait jamais. La même requête a aussi envoyé "id": 1 dans le corps ; l’enregistrement a gardé l’ID 27, et un GET séparé a confirmé que le livre 1 n’avait pas changé.
Un PUT auquel il manque un champ obligatoire échoue avec un 400, comme il se doit : {"price": 35} seul renvoie Missing required field: title.
PATCH ne modifie que les champs que vous envoyez. Cette API utilise JSON merge patch, et sa réponse liste ce qu’elle a modifié et ce qu’elle a ignoré :
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}'{
"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"]
}
}Vérifiez que les deux champs cités ont changé, que tous les autres ont gardé leur ancienne valeur, et que l’id ignoré l’a vraiment été. Puis faites un nouveau GET et vérifiez tout à nouveau. Un PATCH qui met à jour la réponse mais pas l’enregistrement stocké est rare, mais quand cela arrive, c’est exactement le genre de bug qui atteint la production, parce que le client affiche la nouvelle valeur jusqu’au rechargement de la page.
Supprimer, puis supprimer encore
$ 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."}Après une suppression, un GET doit renvoyer 404. Vérifiez aussi que l’enregistrement a disparu de la liste et que le compteur a baissé (/api/Books/countconnectez-vous pour voir renvoie {"count": 25} avec les données de départ).
C’est sur le second DELETE que les API diffèrent. Celle-ci renvoie 404. D’autres renvoient 204 ou 200 à chaque suppression, au motif que l’état final (pas d’enregistrement de ce genre) est le même. Les deux se défendent. Ce que vous cherchez, c’est un 500, ou un 200 qui prétend avoir supprimé quelque chose qui n’existait pas. Envoyez aussi PUT et PATCH à l’ID supprimé. Ici, PATCH reçoit un 404, mais un PUT au corps incomplet reçoit un 400 pour le champ manquant avant que le serveur ne remarque que l’enregistrement n’existe pas. Quelle erreur l’emporte quand deux s’appliquent mérite une phrase dans la documentation, et n’en a souvent aucune.
Créez vos propres données
Il est tentant de tester sur l’enregistrement 1 parce qu’il est toujours là. Ne le faites pas. N’importe qui peut modifier ou supprimer le livre 1 sur un serveur partagé, et un test qui le modifie casse tous les autres tests qui le lisent. Chaque test devrait créer les enregistrements dont il a besoin, ne travailler qu’avec les ID qu’on lui a donnés, et les supprimer à la fin.
Cette API montre bien pourquoi. Elle garde ses données en mémoire et elle est partagée par tous ceux qui l’utilisent. Pendant la rédaction de cette leçon, un autre utilisateur a créé un livre d’ID 26 dans les quelques secondes entre l’une de nos suppressions et la création suivante. Un test qui supposait que "le prochain livre aura l’ID 26" aurait échoué pour des raisons sans aucun rapport avec le code testé.
La même expérience a montré autre chose. Après la suppression du livre 26, le livre créé ensuite a de nouveau reçu l’ID 26. Ce serveur attribue le prochain numéro libre : les ID sont donc réutilisés. Si un autre système avait enregistré "livre 26" avant la suppression, il pointe maintenant vers un autre livre. Que ce soit un bug dépend du produit, mais c’est le genre de constat qui mérite d’être signalé. C’est pour cette raison que la plupart des bases de données ne réutilisent jamais les ID.
Les points d’accès groupés
Beaucoup d’API permettent de créer ou de supprimer plusieurs enregistrements en une requête. La question intéressante est ce qui se passe quand certains éléments sont mauvais.
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"}Le troisième élément a un titre vide, donc toute la requête échoue et l’erreur désigne l’élément par son index. La documentation de Books indique que rien n’est stocké dans ce cas, et vous devez le vérifier : contrôlez que "Bulk A" n’existe pas après la requête en échec. Une API qui enregistre les deux premiers éléments puis échoue laisse le client sans savoir quoi réessayer.
La suppression groupée indique quels ID elle a trouvés et lesquels non :
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]}| Cas groupé | Ce qu’il faut vérifier |
|---|---|
| Tous les éléments valides | Chaque élément est stocké, les ID sont distincts, le compteur augmente du nombre envoyé. |
| Un élément invalide | Rien n’est stocké, ou la réponse dit exactement quels éléments l’ont été. |
| Tableau vide | Un 400 clair, pas un 201 qui n’a rien créé. |
| Au-delà de la limite (100 ici) | Un 400 qui indique la limite. |
| ID en double dans une suppression groupée | Chaque enregistrement supprimé une fois, sans erreur à la seconde occurrence. |
Exercice
Faites tout le cycle de vie sur /api/Moviesconnectez-vous pour voir, en vous servant de sa page de référence pour les champs. Créez un film avec tous les champs remplis, puis :
- Relisez-le et comparez chaque champ avec ce que vous avez envoyé.
- Retrouvez-le avec
?q=et un mot de son titre. - Faites un PUT avec seulement les champs obligatoires. Quels champs ont été réinitialisés ?
- Faites un PATCH d’un champ et envoyez un nom de champ qui n’existe pas. Lisez la liste
ignored. - Supprimez-le, puis faites GET, PATCH et DELETE à nouveau. Notez chaque statut.
Terminez en notant une chose que vous demanderiez aux développeurs (un champ ignoré, un ID réutilisé, une erreur qui l’a emporté sur une autre). Décider quoi signaler fait autant partie du test que les requêtes.