Tester le système : Swagger UI, curl et un script de test en Python

Objectifs de la leçon
  • Tester chaque endpoint dans Swagger UI — Try it out, body, Execute et lecture de la réponse
  • Envoyer des requêtes POST, PUT et DELETE avec curl
  • Écrire un script Python qui exécute une suite de requêtes et vérifie que chaque code d’état est celui attendu
  • Construire une liste de tests manuels (un Test Plan) pour toutes les actions CRUD et les cas limites

Qu’allons-nous apprendre ?

Un serveur qui n’a pas été testé est un serveur dont on ne peut pas être sûr qu’il fonctionne. Nous allons apprendre trois façons, du manuel à l’automatique :

OutilQuand
Swagger UI (/docs)Des tests manuels pratiques, pendant le développement
curlDes tests rapides depuis le terminal, une habitude des gens du backend et du DevOps
Un script PythonUne série de tests qui s’exécute en un clic, et vérifie tout, à chaque fois

Et un Test Plan : ce que l’on teste, et ce que l’on s’attend à obtenir.

Pourquoi en avons-nous besoin ?

Chaque modification du code peut casser quelque chose qui fonctionnait. Si le test est « j’ai ouvert le navigateur et ça a l’air bien », des bugs atteindront les utilisateurs. Une liste fixe de tests, et surtout un script qui l’exécute, permet de modifier le code en toute confiance. C’est aussi un entraînement pour le projet d’intégration, où une journée entière est consacrée au débogage et aux tests manuels de toutes les actions CRUD.

Explication à partir des bases

Swagger UI

Swagger UI est une page que FastAPI construit automatiquement à partir du code : des routes, des Type Hints et des modèles Pydantic. Elle affiche chaque endpoint, et permet d’envoyer de vraies requêtes depuis le navigateur.

http://127.0.0.1:8000/docs

Le processus pour chaque endpoint :

  1. Cliquer sur l’endpoint (par exemple POST /students) l’ouvre.
  2. Try it out → les champs s’ouvrent à la modification.
  3. Remplissez les paramètres, et/ou modifiez le Request body (un exemple de JSON apparaît déjà, construit à partir du modèle !).
  4. Execute → une vraie requête est envoyée au serveur.
  5. En dessous : Curl (la commande équivalente !), la Request URL, le Response body, le Response code et les Response headers.

Un bonus : http://127.0.0.1:8000/openapi.json existe aussi. C’est la description de l’API dans un format standard (OpenAPI), à partir de laquelle Swagger construit la page. D’autres outils savent la lire.

curl — un rappel

curl -i http://127.0.0.1:8000/students
curl -i -X POST http://127.0.0.1:8000/students -H "Content-Type: application/json" -d '{"name": "Tom", "age": 27, "email": "tom@mail.com"}'
curl -i -X DELETE http://127.0.0.1:8000/students/3

-i affiche aussi la ligne d’état, et c’est exactement ce que l’on teste.

curl avec du JSON dans PowerShell

Dans PowerShell, les guillemets dans le JSON sont compliqués. Deux façons pratiques : (1) enregistrez le JSON dans un fichier (new_student.json) et envoyez-le avec curl.exe -i -X POST URL -H "Content-Type: application/json" -d "@new_student.json". (2) Copiez la commande curl que Swagger affiche et exécutez-la dans Git Bash.

Un script de test en Python

Un script qui envoie des requêtes à un serveur en cours d’exécution, et vérifie pour chacune : le code est-il celui attendu ? À la fin, il affiche combien de tests ont réussi. Un test comme celui-ci, qui fait fonctionner le système de l’extérieur comme un vrai client, s’appelle un test End-to-End. (Plus tard dans le parcours, vous apprendrez des outils de test professionnels, comme pytest. L’idée est la même.)

Test Plan — une liste de tests

Pour chaque endpoint : le cas où tout va bien + chaque cas d’échec du tableau de correspondance. Si chaque ligne du tableau a un test, vous savez que le contrat de l’API est respecté.

Syntaxe

import json
import urllib.error
import urllib.request

BASE_URL = "http://127.0.0.1:8000"

def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(BASE_URL + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
    try:
        response = urllib.request.urlopen(request)
        status, raw = response.status, response.read()
    except urllib.error.HTTPError as error:
        status, raw = error.code, error.read()
    return status, (json.loads(raw) if raw else None)

call renvoie un tuple (le code, le corps de la réponse), en cas de succès comme d’erreur (HTTPError). Un corps vide (204) → None.

Premier exemple de code

Swagger — créer un étudiant étape par étape :

  1. Lancez le serveur (uvicorn main:app --reload) et ouvrez /docs.
  2. POST /students → Try it out. Le Request body affiche :
{
  "name": "string",
  "age": 0,
  "email": "string"
}
  1. Remplacez-le par :
{
  "name": "Tom",
  "age": 27,
  "email": "tom@mail.com"
}
  1. Execute. Le résultat :
Code: 201
Response body:
{
  "id": 3,
  "name": "Tom",
  "age": 27,
  "email": "tom@mail.com"
}
  1. Cliquez de nouveau sur Execute, avec le même body. Maintenant : 409, "a student with email tom@mail.com already exists".

Analyse du code ligne par ligne

Ce que l’on voit dans SwaggerD’où cela vient dans le code
La liste de tous les endpointsLes décorateurs @app.get/post/put/delete
Le nom de l’API en haut (« Students API »)FastAPI(title="Students API")
L’exemple de JSON dans le bodyLe modèle StudentIn (champs et types)
Le champ student_id avec integerLe Type Hint student_id: int
Le code 201 comme « Successful Response » dans POSTstatus_code=201
L’id 3 dans la réponsedb.create_student → AUTO_INCREMENT

Tout est dérivé du code. Quand vous modifiez une route, la documentation se met à jour toute seule. C’est pourquoi elle est toujours exacte.

Un autre exemple

Un script de test de bout en bout. smoke_test.py (le serveur doit tourner dans un autre terminal, sur une table vide : dans Database Client, exécutez DROP TABLE students;, et redémarrez le serveur — init_db recrée la table) :

import json
import urllib.error
import urllib.request

BASE_URL = "http://127.0.0.1:8000"

def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(BASE_URL + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
    try:
        response = urllib.request.urlopen(request)
        status, raw = response.status, response.read()
    except urllib.error.HTTPError as error:
        status, raw = error.code, error.read()
    return status, (json.loads(raw) if raw else None)

CHECKS = [
    ("create Dana", "POST", "/students", {"name": "Dana", "age": 24, "email": "dana@mail.com"}, 201),
    ("create Avi", "POST", "/students", {"name": "Avi", "age": 31, "email": "avi@mail.com"}, 201),
    ("duplicate email", "POST", "/students", {"name": "Dana Copy", "age": 20, "email": "dana@mail.com"}, 409),
    ("too young", "POST", "/students", {"name": "Kid", "age": 7, "email": "kid@mail.com"}, 400),
    ("missing fields", "POST", "/students", {"name": "Noa"}, 422),
    ("list", "GET", "/students", None, 200),
    ("get existing", "GET", "/students/1", None, 200),
    ("get missing", "GET", "/students/99", None, 404),
    ("bad id", "GET", "/students/abc", None, 422),
    ("update", "PUT", "/students/2", {"name": "Avi Levi", "age": 32, "email": "avi@mail.com"}, 200),
    ("update missing", "PUT", "/students/99", {"name": "Ghost", "age": 30, "email": "g@mail.com"}, 404),
    ("update clash", "PUT", "/students/2", {"name": "Avi", "age": 32, "email": "dana@mail.com"}, 409),
    ("delete", "DELETE", "/students/1", None, 204),
    ("delete again", "DELETE", "/students/1", None, 404),
]

def main():
    passed = 0
    for name, method, path, body, expected in CHECKS:
        status, data = call(method, path, body)
        ok = status == expected
        passed = passed + (1 if ok else 0)
        mark = "PASS" if ok else "FAIL"
        print(mark, "|", name, "|", method, path, "| expected", expected, "got", status)
    print(passed, "/", len(CHECKS), "checks passed")

if __name__ == "__main__":
    main()

Sortie :

PASS | create Dana | POST /students | expected 201 got 201
PASS | create Avi | POST /students | expected 201 got 201
PASS | duplicate email | POST /students | expected 409 got 409
PASS | too young | POST /students | expected 400 got 400
PASS | missing fields | POST /students | expected 422 got 422
PASS | list | GET /students | expected 200 got 200
PASS | get existing | GET /students/1 | expected 200 got 200
PASS | get missing | GET /students/99 | expected 404 got 404
PASS | bad id | GET /students/abc | expected 422 got 422
PASS | update | PUT /students/2 | expected 200 got 200
PASS | update missing | PUT /students/99 | expected 404 got 404
PASS | update clash | PUT /students/2 | expected 409 got 409
PASS | delete | DELETE /students/1 | expected 204 got 204
PASS | delete again | DELETE /students/1 | expected 404 got 404
14 / 14 checks passed

Analyse :

  • CHECKS — une liste de tuples : le nom du test, la méthode, le chemin, le body et le code attendu. Les données sont séparées de la logique (la séparation des responsabilités !). Ajouter un test = ajouter une ligne.
  • L’ordre compte : les tests dépendent les uns des autres (on crée d’abord, puis on supprime). C’est pourquoi on part d’une base vide.
  • La boucle — envoie, compare avec expected, et affiche PASS/FAIL.
  • La magie : vous changez quelque chose dans le code (un refactoring), vous lancez, et en une seconde vous voyez si quelque chose a cassé. Un filet de sécurité.

Essayez de le casser : dans main.py, changez status_code=204 en 200 dans DELETE, et enregistrez (--reload). Supprimez la table (DROP TABLE students; dans Database Client), redémarrez, et lancez le script :

FAIL | delete | DELETE /students/1 | expected 204 got 200
13 / 14 checks passed

Le script a détecté la modification. Remettez le code en place.

Erreur courante

Erreur 1 : ne tester que le cas où tout va bien. « J’ai créé un étudiant, et ça a marché » ≠ « le serveur fonctionne ». La plupart des bugs du code serveur sont dans les cas d’échec : un 404 qui revient en 200, un 409 qui revient en 500. Test Plan = le cas où tout va bien + chaque ligne d’échec du tableau de correspondance.

Erreur 2 : lancer le script de test sur une base contenant des données précédentes. « create Dana » échouera avec 409, car Dana existe déjà depuis l’exécution précédente. Une base vide avant chaque exécution (ou, plus tard dans le parcours, une base de test séparée).

Erreur 3 : oublier que le serveur doit tourner.

urllib.error.URLError: <urlopen error [WinError 10061] No connection could be made because the target machine actively refused it>

« Connection refused » = aucun serveur n’écoute sur le port (souvenez-vous du thème des réseaux !). Lancez uvicorn dans un terminal séparé.

Erreur 4 : ne vérifier que le code, et pas le corps. Un code 200 avec un corps faux (un nom qui n’a pas été mis à jour) reste un bug. Dans les tests importants, vérifiez aussi les valeurs : data["name"] == "Avi Levi". (Voir la pratique en autonomie.)

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: Test Plan

Écrivez un Test Plan pour l’API des étudiants : au moins 8 tests, avec la méthode, le chemin, ce que vous envoyez, et le code que vous vous attendez à obtenir. Incluez le cas où tout va bien et aussi des cas d’échec.

Afficher la solution
#TestRequêteCode attendu
1Créer DanaPOST /students avec un corps valide201
2Un e-mail en doublePOST /students avec l’e-mail de Dana409
3Trop jeunePOST /students avec l’âge 7400
4Des champs manquantsPOST /students avec seulement un nom422
5Un étudiant existantGET /students/1200
6Un étudiant qui n’existe pasGET /students/99404
7Un id non numériqueGET /students/abc422
8SupprimerDELETE /students/1204
9Supprimer à nouveauDELETE /students/1404

Le premier test réussit toujours — la plupart des bugs sont dans les cas d’échec : un 404 qui revient en 200, ou un 409 qui revient en 500.

Exercice 2: curl

Écrivez des commandes curl pour un serveur tournant sur http://127.0.0.1:8000 :

  1. Créer un étudiant (Dana, 24, dana@mail.com) — en affichant le code.
  2. Lire l’étudiant 1.
  3. Supprimer l’étudiant 1 — en affichant le code.
Afficher la solution
curl -i -X POST http://127.0.0.1:8000/students -H "Content-Type: application/json" -d '{"name": "Dana", "age": 24, "email": "dana@mail.com"}'
curl http://127.0.0.1:8000/students/1
curl -i -X DELETE http://127.0.0.1:8000/students/1

-i affiche le code d’état — dans les tests, c’est la chose la plus importante à voir. Dans Swagger (/docs), vous pouvez effectuer les mêmes requêtes depuis le navigateur.

Exercice 3: La fonction call

Écrivez une fonction call(method, path, body=None) qui envoie une requête au serveur avec urllib.request, et renvoie un tuple (le code, le corps sous forme de dictionnaire ou None). Les erreurs HTTP (4xx, 5xx) ne doivent pas faire planter — elles renvoient aussi un code.

Afficher la solution
import json
import urllib.error
import urllib.request

BASE_URL = "http://127.0.0.1:8000"


def call(method, path, body=None):
    data = None
    if body is not None:
        data = json.dumps(body).encode()
    request = urllib.request.Request(BASE_URL + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
    try:
        response = urllib.request.urlopen(request)
        status, raw = response.status, response.read()
    except urllib.error.HTTPError as error:
        status, raw = error.code, error.read()
    if raw:
        return status, json.loads(raw)
    return status, None
  • urlopen lance une HTTPError sur les 4xx et 5xx — et dans les tests, ce sont justement les réponses qui nous intéressent, on les attrape donc et on renvoie le code.
  • Une réponse 204 n’a pas de corps — donc None.

Exercice 4: Le script qui affiche PASS / FAIL

Écrivez la partie qui parcourt la liste des tests et affiche un résultat. Pour vous entraîner sans serveur, les résultats sont déjà dans la liste : le nom du test, le code attendu et le code reçu.

results = [
    ("create Dana", 201, 201),
    ("duplicate email", 409, 409),
    ("get missing", 404, 200),
    ("delete", 204, 204),
]

Résultat attendu :

PASS | create Dana | expected 201 got 201
PASS | duplicate email | expected 409 got 409
FAIL | get missing | expected 404 got 200
PASS | delete | expected 204 got 204
3 / 4 checks passed
Afficher la solution
results = [
    ("create Dana", 201, 201),
    ("duplicate email", 409, 409),
    ("get missing", 404, 200),
    ("delete", 204, 204),
]

passed = 0
for name, expected, got in results:
    if expected == got:
        status = "PASS"
        passed = passed + 1
    else:
        status = "FAIL"
    print(status, "|", name, "| expected", expected, "got", got)
print(passed, "/", len(results), "checks passed")
PASS | create Dana | expected 201 got 201
PASS | duplicate email | expected 409 got 409
FAIL | get missing | expected 404 got 200
PASS | delete | expected 204 got 204
3 / 4 checks passed

Avec un vrai serveur, got vient de call(...). Le FAIL révèle ici un vrai bug : un étudiant qui n’existe pas revient en 200 au lieu de 404.

Exercice 5: Le serveur ne tourne pas

Vous lancez le script de test, et vous obtenez :

urllib.error.URLError: <urlopen error [WinError 10061] No connection could be made
because the target machine actively refused it>
  1. Qu’est-ce que cela signifie ?
  2. Comment lancer correctement les tests ?
  3. Pourquoi vaut-il la peine de partir d’une base vide ?
Afficher la solution
  1. Aucun serveur n’écoute à l’adresse et au port — la connexion a été refusée. Le script est un client, et il a besoin d’un serveur en marche.
  2. Deux terminaux : dans l’un, lancez uvicorn main:app, et dans l’autre le script de test.
  3. Pour que les résultats soient prévisibles : l’id du premier étudiant vaut 1, et la liste commence vide. Sur une base avec d’anciennes données, « créer Dana » peut renvoyer 409 parce qu’elle existe déjà — et le test échouera sans qu’il y ait de bug.

Résumé

  • Swagger UI (/docs) : Try it out → body → Execute. Construit automatiquement à partir du code, et affiche aussi la commande curl équivalente.
  • curl -i : un test rapide depuis le terminal. Dans PowerShell, du JSON depuis un fichier (-d "@file.json") ou Git Bash.
  • Un script Python (End-to-End) : une liste de tests (les données) + une boucle (la logique) → PASS/FAIL. Un filet de sécurité pour chaque modification.
  • Test Plan = le cas où tout va bien + chaque cas d’échec du tableau de correspondance. Vérifiez les codes et aussi le corps.
  • Lancez les tests sur une base vide, et avec un serveur en marche.

Documentation officielle