Écrire vos premiers cas de test
Résultats attendus, tests négatifs, valeurs limites et classes d’équivalence.
Jusqu’ici, vous avez envoyé des requêtes et regardé ce qui revenait. Le test commence quand vous décidez, avant d’envoyer, ce qui devrait revenir, puis comparez. Cette décision mise par écrit, c’est un cas de test. Cette leçon porte sur la façon de les écrire, et de choisir lesquels écrire, car vous ne pouvez pas envoyer toutes les requêtes possibles.
Ce que contient un cas de test
Les équipes utilisent des modèles différents, mais presque tous ont les mêmes parties :
| Partie | Ce qu’on y met | Exemple |
|---|---|---|
| ID | Une référence courte pour pouvoir en parler | PROD-GET-03 |
| Titre | Ce qui est vérifié, en une ligne | Un ID de produit inconnu renvoie 404 |
| Préconditions | Ce qui doit être vrai avant de commencer | Aucun produit d’ID 9999 n’existe |
| Requête | Méthode, URL, en-têtes et corps | GET /api/Products/9999 |
| Résultat attendu | Statut, en-têtes importants, corps | 404, corps JSON dont le message nomme l’ID 9999 |
| Résultat obtenu | Rempli à l’exécution | Conforme |
Le résultat attendu est la partie que l’on écrit mal. "Renvoie une erreur" n’est pas un résultat attendu, parce que presque tout le satisfait. Soyez précis sur le code de statut, les en-têtes qui comptent (en général Content-Type, parfois des en-têtes de cache ou de localisation) et le corps : quels champs doivent être présents, avec quels types et quelles valeurs. Si vous ne savez pas dire quelle est la bonne réponse, vous avez trouvé une question pour le développeur ou le product owner, et elle mérite d’être posée avant de tester quoi que ce soit.
D’où vient le résultat attendu ? De la documentation, du ticket qui décrivait la fonctionnalité, du comportement de points d’accès similaires de la même API, et parfois du simple bon sens (un prix ne devrait pas être négatif). Quand ces sources divergent, notez celle que vous avez suivie.
Tests positifs et négatifs
Un test positif envoie une requête valide et vérifie qu’elle fonctionne : le produit 1 existe, donc GET /api/Products/1 le renvoie. Un test négatif envoie quelque chose que l’API devrait refuser et vérifie qu’elle le refuse correctement : le bon statut, un message utile, et aucun effet de bord.
Les débutants écrivent surtout des tests positifs, parce qu’ils suivent la description de la fonctionnalité. Les développeurs ont généralement essayé eux-mêmes le chemin positif avant de livrer. C’est dans les tests négatifs qu’on trouve des choses. En gros, attendez-vous à écrire plus de tests négatifs que de positifs pour tout point d’accès qui prend des entrées.
Les classes d’équivalence
Le point d’accès de liste GET /api/Products accepte un paramètre limit. La référence de l’API dit que c’est la taille de page, de 1 à 100. Il y a une infinité de valeurs possibles. Le partitionnement en classes d’équivalence consiste à regrouper les entrées que le serveur devrait traiter de la même façon, et à tester une valeur de chaque groupe plutôt que toutes :
| Classe | Valeur d’exemple | Attendu |
|---|---|---|
| Valide, dans la plage | limit=10 | Dix produits |
| Sous la plage | limit=-5 | Refusé, ou ramené au minimum |
| Au-dessus de la plage | limit=500 | Refusé, ou ramené à 100 |
| Pas un nombre | limit=abc | Refusé avec 400 |
| Pas un entier | limit=2.5 | Refusé avec 400 |
| Vide | limit= | Traité comme absent |
| Absent | pas de limit | Tous les produits, comme documenté |
Remarquez les entrées "refusé, ou ramené". La documentation ne dit pas ce qui se passe hors de la plage : il y a donc deux comportements raisonnables. Les deux se défendent. Ce que vous testez, c’est que l’API en applique un, de façon cohérente, sans planter ni renvoyer quelque chose d’absurde.
Les valeurs limites
Les bugs se concentrent aux bords des plages, parce que c’est là que les développeurs écrivent < quand ils voulaient <=. L’analyse des valeurs limites consiste à tester la limite et un pas de chaque côté. Pour une plage de 1 à 100, cela donne 0, 1, 2, 99, 100 et 101. Voici ce que fait réellement cette API avec ces valeurs et les autres classes. L’en-tête X-Per-Page indique la taille de page utilisée par le serveur.
| Requête | Statut | Éléments renvoyés | X-Per-Page |
|---|---|---|---|
?limit=0 | 200 | 1 | 1 |
?limit=1 | 200 | 1 | 1 |
?limit=-1 | 200 | 1 | 1 |
?limit=100 | 200 | 20 (tous) | 100 |
?limit=101 | 200 | 20 (tous) | 100 |
?limit=abc | 200 | 20 | 20 |
?limit=2.5 | 200 | 20 | 20 |
?limit= | 200 | 20 | 20 |
/api/Products?limit=0Lecture des résultats : les valeurs inférieures à 1 sont ramenées à 1, celles supérieures à 100 sont ramenées à 100, et tout ce qui n’est pas un entier est ignoré comme si limit était absent. Rien de tout cela n’est écrit, mais c’est cohérent, donc probablement voulu. Deux points méritent quand même une note dans votre rapport de test. Que limit=0 renvoie un élément surprend : un client qui demande zéro élément (pour lire le total à moindre coût, par exemple) reçoit un produit qu’il ne voulait pas. Et que limit=abc soit ignoré en silence signifie qu’une faute de frappe dans un client passe inaperçue, là où un 400 l’aurait attrapée.
Il y a aussi ici une leçon sur les données de test. L’API n’a que 20 produits, donc limit=100 et limit=101 renvoient le même corps. Le corps ne permettait pas de voir que 101 avait été ramené. L’en-tête, si. Quand une limite dépasse vos données, trouvez un autre signal de ce genre ou créez assez d’enregistrements pour l’atteindre.
Les limites se cachent aussi dans les ID
La référence indique que l’id de /api/Products/{id} est un int32, un entier sur 32 bits dont la plus grande valeur est 2 147 483 647. C’est une limite qui vaut d’être testée :
| Requête | Statut | Corps |
|---|---|---|
/api/Products/20 | 200 | Le produit 20, le dernier |
/api/Products/21 | 404 | Message d’erreur JSON |
/api/Products/0 | 404 | Message d’erreur JSON |
/api/Products/-1 | 404 | Message d’erreur JSON |
/api/Products/2147483647 | 404 | Message d’erreur JSON |
/api/Products/2147483648 | 404 | Vide |
/api/Products/abc | 404 | Vide |
/api/Products/1.5 | 404 | Vide |
/api/Products/01 | 200 | Le produit 1 |
Deux choses ressortent. Tout ce qui n’est pas un int32 valide reçoit un 404 sans aucun corps, alors que les ID valides mais inexistants reçoivent une erreur JSON. Et 01 est accepté comme 1. Le second point est sans danger ici, mais sur une API qui met les réponses en cache par URL, /1 et /01 seraient mis en cache séparément. Aucun des deux résultats n’est clairement faux. Les deux valent la peine d’être connus.
Un jeu de cas de test pour un point d’accès
Au total, un premier jeu raisonnable de cas de test pour GET /api/Products/{id} ressemble à ceci. Ce n’est pas tout ce que vous pourriez tester, mais cela couvre chaque classe une fois et chaque limite.
| ID | Requête | Résultat attendu |
|---|---|---|
| PROD-GET-01 | GET /api/Products/1 | 200, JSON, id vaut 1, title est une chaîne non vide, price est un nombre supérieur à 0 |
| PROD-GET-02 | GET /api/Products/20 | 200, le plus grand ID de départ |
| PROD-GET-03 | GET /api/Products/21 | 404, corps d’erreur JSON nommant l’ID 21 |
| PROD-GET-04 | GET /api/Products/0 | 404, corps d’erreur JSON |
| PROD-GET-05 | GET /api/Products/-1 | 404, corps d’erreur JSON |
| PROD-GET-06 | GET /api/Products/abc | 400 ou 404, avec un corps d’erreur JSON |
| PROD-GET-07 | GET /api/Products/2147483648 | 400 ou 404, avec un corps d’erreur JSON |
| PROD-GET-08 | HEAD /api/Products/1 | Même statut et mêmes en-têtes que GET, sans corps |
| PROD-GET-09 | GET /api/Products/1, deux fois | Corps identiques les deux fois |
PROD-GET-06 et 07 échouent sur l’API actuelle, parce que le corps est vide. PROD-GET-08 échoue aussi : HEAD /api/Products/1 renvoie 405 Method Not Allowed, alors que les ressources plus récentes comme /api/Books/{id}connectez-vous pour voir acceptent HEAD. Savoir si HEAD est attendu ici est une question pour les responsables de l’API. Ce n’est pas grave. Un cas de test consigne ce qui devrait se passer ; un cas de test qui échoue, c’est la façon de découvrir que ce n’est pas le cas. PROD-GET-02 a un point faible : si quelqu’un supprime le produit 20 ou ajoute le produit 21, il ne veut plus dire ce qu’il dit. La leçon suivante montre comment créer vos propres données de test pour que les tests ne dépendent pas d’enregistrements contrôlés par quelqu’un d’autre.
Rédiger un rapport de bug
Quand un cas de test échoue, vous le documentez. Un bon rapport de bug permet à un développeur de reproduire le problème en moins d’une minute sans rien vous demander. En prenant pour exemple les 404 vides ci-dessus, comparez d’abord les deux réponses :
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
{"status":404,"error":"Not Found","message":"Product with ID 9999 does not exist."}HTTP/1.1 404 Not Found
Content-Length: 0Un rapport pourrait dire (ici en anglais, comme beaucoup d’équipes les rédigent) :
Title: GET /api/Products/{id} returns an empty 404 for non-numeric
and out-of-range IDs, instead of the JSON error body
Environment: https://api.testingapis.com, 30 Sep 2026
Steps:
curl -i https://api.testingapis.com/api/Products/9999
curl -i https://api.testingapis.com/api/Products/abc
curl -i https://api.testingapis.com/api/Products/2147483648
Expected:
All three return 404 with Content-Type: application/json and a body
like {"status":404,"error":"Not Found","message":"..."}, as documented
under "Responses and errors" in the API reference.
Actual:
9999 404, JSON body with a message
abc 404, Content-Length: 0, no Content-Type, no body
2147483648 404, empty body (one above the int32 maximum; 2147483647
returns the JSON error)
Impact:
Clients that parse every error body as JSON fail on the empty
responses. The empty 404 also looks like "this URL does not exist"
rather than "this product does not exist".
Notes:
Probably the route only matches int32 values, so other input never
reaches the controller. Could be intended; if so, the docs should say
which errors have no body.Ce qui rend ce rapport utile :
- Le titre dit ce qui ne va pas et où, il reste donc clair dans une liste de cinquante tickets.
- Les étapes sont des commandes à coller. Pas de "appelez le point d’accès produits avec un mauvais ID".
- Le comportement attendu renvoie à une source (la documentation), ce n’est donc pas qu’une préférence personnelle.
- Il montre le schéma plutôt qu’un seul exemple. Trois requêtes, dont une qui marche pour comparer, en disent plus qu’une requête qui échoue.
- Il indique l’impact sur un vrai client. Les développeurs priorisent selon l’impact, et "incohérent" tout seul est rarement corrigé.
- Il admet l’incertitude. Le comportement peut être voulu, et le rapport dit ce qui réglerait la question dans un sens comme dans l’autre (corriger le code ou corriger la documentation).
Exercice
Choisissez GET /api/Books/{id}connectez-vous pour voir ou GET /api/Countries/code/{code}connectez-vous pour voir. Cherchez le point d’accès dans la référence de l’API, puis :
- Listez les classes d’équivalence du paramètre de chemin.
- Choisissez les valeurs limites. Pour les codes pays, pensez à la longueur et à la casse.
- Écrivez au moins huit cas de test au format du tableau ci-dessus, avec des résultats attendus précis.
- Lancez-les avec curl et remplissez les résultats obtenus.
Si quelque chose se comporte autrement que prévu, rédigez un rapport de bug selon la structure ci-dessus, en incluant ce dont vous n’êtes pas sûr.