Cours Python · Module 6: Async et FastAPI

Les tests - vérifier la stabilité

5 min de lecture
Dans cette leçon4

Bienvenue ! Ici Darwin, avec les tests pour FastAPI !

Imagine que tu corriges une ligne dans l'endpoint de connexion et qu'après le déploiement, la liste des espèces ne fonctionne plus. Personne n'a vérifié, parce que "le changement était minuscule". Les tests automatisés attrapent ce genre de bévue en quelques secondes, avant que quiconque d'autre que toi ne la voie.

Les tests sont une partie critique des applications en production. FastAPI offre un excellent support de pytest !

Analogie safari : Tester, c'est comme vérifier l'équipement avant un safari - on s'assure que le GPS fonctionne, que le véhicule est en état, que la radio capte ! Mieux vaut découvrir les problèmes avant l'excursion !

Installation

On installe trois paquets : pytest lance les tests, httpx est le client HTTP sur lequel FastAPI construit son client de test, et pytest-asyncio permet d'écrire des tests sous forme de fonctions async def :

1pip install pytest httpx pytest-asyncio

Rien d'autre à configurer. pytest trouve tout seul les fichiers nommés test_*.py ou *_test.py et y exécute les fonctions dont le nom commence par test.

Un test FastAPI de base

TestClient simule des requêtes HTTP vers l'application sans démarrer de serveur : pas besoin d'uvicorn ni d'un port libre. Tu le crées une fois en lui passant l'objet app, et le premier test peut vérifier la route racine :

1# test_main.py
2from fastapi.testclient import TestClient
3from main import app
4
5client = TestClient(app)
6
7def test_root():
8    response = client.get("/")
9    assert response.status_code == 200
10    assert response.json() == {"message": "Hello Safari"}

Le test est une fonction def ordinaire, même si les endpoints de l'application sont async : TestClient s'occupe lui-même de la boucle d'événements. Le mot-clé assert vérifie une condition, et quand elle échoue, pytest affiche les deux valeurs comparées, donc tu vois tout de suite ce qui cloche.

Les tests suivants vérifient la liste des espèces et un enregistrement isolé. En plus du code de statut, ça vaut le coup de regarder le contenu de la réponse :

1def test_list_species():
2    response = client.get("/species")
3    assert response.status_code == 200
4    assert isinstance(response.json(), list)
5
6def test_get_species():
7    response = client.get("/species/1")
8    assert response.status_code == 200
9    data = response.json()
10    assert data["id"] == 1
11    assert "name" in data

response.json() transforme la réponse en dict ou en liste Python. On ne compare pas tout l'enregistrement, on vérifie seulement ce qui compte vraiment : le type du résultat, l'id et la présence du champ name.

Le test de création d'une espèce envoie le corps de la requête via le paramètre json :

1def test_create_species():
2    response = client.post("/species", json={
3        "name": "Cheetah",
4        "scientific_name": "Acinonyx jubatus",
5        "population": 7100,
6        "habitat": "savanna"
7    })
8    assert response.status_code == 201
9    assert response.json()["name"] == "Cheetah"

Attention au piège : par défaut, FastAPI renvoie 200 pour un POST. Ce test ne passe que si l'endpoint déclare @app.post("/species", status_code=201). Le code 201 Created décrit mieux la création d'une ressource, donc je te conseille d'ajouter ce paramètre dans l'application plutôt que d'affaiblir le test.

Le dernier test vérifie le chemin d'erreur :

1def test_species_not_found():
2    response = client.get("/species/999")
3    assert response.status_code == 404

Tester les cas négatifs est aussi important que tester les cas positifs. L'espèce d'id 999 n'existe pas, donc on attend 404, et non 500 ou une réponse vide.

Lance les tests :

Une seule commande dans le dossier du projet suffit :

1pytest

Chaque point dans la sortie est un test réussi, et la lettre F signale un échec avec une description détaillée en dessous. L'option -v affiche le nom de chaque test.

Tests asynchrones

Parfois, le test lui-même doit être asynchrone, par exemple quand il appelle des fonctions async liées à la base. On utilise alors AsyncClient de httpx au lieu de TestClient, et le décorateur @pytest.mark.asyncio du paquet pytest-asyncio indique à pytest que la fonction doit tourner dans une boucle d'événements :

1import pytest
2from httpx import ASGITransport, AsyncClient
3from main import app
4
5@pytest.mark.asyncio
6async def test_list_species_async():
7    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
8        response = await client.get("/species")
9        assert response.status_code == 200

ASGITransport relie le client directement à l'application, sans réseau, et base_url peut être n'importe quoi, puisqu'aucune requête ne sort du processus. Dans du code plus ancien, y compris les exercices, tu croiseras le raccourci AsyncClient(app=app). httpx l'a déprécié en version 0.27 et supprimé en version 0.28 : aujourd'hui, il provoque une TypeError. La documentation de FastAPI utilise le décorateur @pytest.mark.anyio au lieu de pytest-asyncio - les deux approches fonctionnent, choisis-en une et tiens-t'y dans tout le projet.

Les bonnes habitudes

Les tests ci-dessus supposent que la base contient une espèce d'id 1. C'est une hypothèse fragile : le résultat dépend de ce qui se trouve dans la base à ce moment-là. En pratique, les tests utilisent une base de test séparée, et FastAPI permet de remplacer la dépendance get_db via app.dependency_overrides, pour que les endpoints reçoivent une session de test sans toucher à leur code. Écris les tests en même temps que la fonctionnalité, pas "un jour" : plus tu attends, plus ils sont difficiles à ajouter.

Prochaine leçon : le déploiement en production ! Des tests au vert, c'est le laissez-passer sans lequel une application ne devrait pas partir dans le cloud.

Retiens : un test, c'est la vérification du matériel avant le départ - dix minutes au camp évitent des heures dans la savane avec une radio en panne.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. À quoi sert pytest-asyncio ?

  2. 2. Que fournit TestClient dans FastAPI ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne un test asynchrone d'un endpoint :

  • Clique dans l'ordre

    Ordonne l'assertion qui vérifie le code de statut :

  • Éditeur de code

    Crée un test async qui vérifie que GET /species renvoie le statut 200 et une liste.

Articles utiles