Aller au contenu principal
FastAPIPythonDockerJWTFormation

Tests et documentation automatique

30 min de lecture Apprendre FastAPI — Chapitre 5

Sécurise ton API avec JWT et OAuth2, écris des tests automatisés avec pytest, et génère la documentation OpenAPI/Swagger automatiquement.

Ton API fonctionne, le CRUD est connecté à PostgreSQL. Mais sans tests, chaque modification est un pari. Et sans documentation, tes consommateurs devinent comment l’utiliser. Ce chapitre couvre les deux piliers qui transforment une API “ça marche sur ma machine” en API de production : pytest + TestClient pour les tests, et la documentation OpenAPI que FastAPI génère automatiquement.

Pourquoi tester une API

Un test API vérifie que ton endpoint renvoie la bonne réponse pour une entrée donnée. C’est ton filet de sécurité :

  • Confiance — tu déploies en sachant que le CRUD fonctionne
  • Régression — un changement dans crud.py ne casse pas silencieusement /users
  • Documentation vivante — les tests montrent exactement comment l’API se comporte
  • Refactoring serein — tu changes la structure interne sans peur

💡 Règle du 80/20 : teste d’abord les endpoints (tests d’intégration). Les tests unitaires sur les fonctions CRUD viendront après, quand ta couverture de base est solide.

Setup de test avec pytest

Installe les dépendances et configure une base de test isolée :

pip install pytest httpx pytest-cov

Le fichier conftest.py est le cœur du setup — il crée une base SQLite en mémoire et un client de test qui n’a pas besoin de serveur HTTP :

# conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from main import app
from database import Base, get_db

engine = create_engine("sqlite:///./test.db",
                       connect_args={"check_same_thread": False})
TestSession = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def override_get_db():
    db = TestSession()
    try:
        yield db
    finally:
        db.close()

@pytest.fixture(autouse=True)
def setup_db():
    Base.metadata.create_all(bind=engine)
    yield
    Base.metadata.drop_all(bind=engine)

@pytest.fixture
def client():
    app.dependency_overrides[get_db] = override_get_db
    with TestClient(app) as c:
        yield c
    app.dependency_overrides.clear()

Points clés :

  • dependency_overrides remplace get_db par la version test — pas besoin de PostgreSQL
  • autouse=True recrée les tables avant chaque test — isolation totale
  • TestClient exécute les requêtes sans démarrer Uvicorn — rapide et fiable

🔥 Chaque test part d’une base vide. C’est intentionnel : un test qui dépend des données d’un autre test est fragile et casse de manière imprévisible.

Écrire des tests efficaces

Tester le happy path et les erreurs

Un bon test couvre les deux côtés — ce qui marche et ce qui doit échouer :

# test_users.py
def test_create_user(client):
    response = client.post("/users", json={
        "name": "Alice", "email": "alice@ex.com", "password": "secret123"
    })
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Alice"
    assert "id" in data
    assert "password" not in data  # jamais exposé

def test_create_user_duplicate_email(client):
    user = {"name": "Alice", "email": "alice@ex.com", "password": "secret123"}
    client.post("/users", json=user)
    response = client.post("/users", json=user)
    assert response.status_code == 400

def test_get_user_not_found(client):
    response = client.get("/users/9999")
    assert response.status_code == 404

def test_create_user_invalid(client):
    response = client.post("/users", json={"name": "", "email": "x"})
    assert response.status_code == 422

Quatre tests, quatre scénarios : création OK, doublon rejeté, ressource introuvable, données invalides. C’est la base pour chaque ressource CRUD.

Tester les routes protégées

Si ton API utilise l’authentification JWT, crée une fixture qui fournit les headers :

@pytest.fixture
def auth_headers(client):
    client.post("/users", json={
        "name": "Test", "email": "test@ex.com", "password": "secret123"
    })
    resp = client.post("/token", data={
        "username": "test@ex.com", "password": "secret123"
    })
    token = resp.json()["access_token"]
    return {"Authorization": f"Bearer {token}"}

def test_create_article_authenticated(client, auth_headers):
    response = client.post("/users/1/articles", json={
        "title": "Mon article", "content": "Contenu suffisant"
    }, headers=auth_headers)
    assert response.status_code == 201

def test_create_article_no_token(client):
    response = client.post("/users/1/articles", json={
        "title": "Test", "content": "Contenu"
    })
    assert response.status_code == 401

⚠️ Teste toujours l’accès sans token. C’est le test le plus important en sécurité — il vérifie que tes routes protégées le sont vraiment.

Lancer et mesurer :

# Tous les tests avec détails
pytest -v

# Avec couverture de code
pytest --cov=. --cov-report=term-missing -v

# Un test spécifique
pytest tests/test_users.py::test_create_user -v

🎯 Vise 80% de couverture minimum sur les endpoints. 100% est rarement rentable — mais en dessous de 80%, tu navigues à l’aveugle.

Documentation OpenAPI automatique

FastAPI génère trois interfaces sans aucune configuration :

  • /docs — Swagger UI, interactive, permet de tester les endpoints
  • /redoc — ReDoc, lisible, idéale pour les consommateurs
  • /openapi.json — le schéma brut, utilisable par les générateurs de SDK

Tout ce que tu mets dans tes modèles Pydantic (Field, description, example) et tes décorateurs (response_model, status_code, tags) apparaît automatiquement dans la doc.

Personnaliser la documentation

app = FastAPI(
    title="Blog API",
    description="API de blog : articles, utilisateurs, authentification JWT.",
    version="2.0.0",
    contact={"name": "DevOps Lab", "url": "https://devopslab.ch"},
    openapi_tags=[
        {"name": "users", "description": "Gestion des utilisateurs"},
        {"name": "articles", "description": "CRUD articles"},
        {"name": "auth", "description": "Authentification JWT"},
    ]
)

@app.get("/articles/{article_id}", tags=["articles"],
         response_model=ArticleResponse,
         responses={404: {"description": "Article non trouvé"}})
def get_article(article_id: int, db: Session = Depends(get_db)):
    """Récupère un article par son ID."""
    ...

Les tags regroupent les endpoints par domaine dans Swagger. Les responses documentent les codes d’erreur possibles. Les docstrings deviennent les descriptions des endpoints.

💡 Désactive la doc en production si ton API n’est pas publique : app = FastAPI(docs_url=None, redoc_url=None). La doc expose la surface d’attaque de ton API.

Les pièges classiques

Tests qui dépendent de l’ordre d’exécution. Si test_delete suppose que test_create a tourné avant, ton test est fragile. Chaque test doit créer ses propres données — c’est pour ça qu’on recrée la base à chaque test.

Tester l’implémentation au lieu du comportement. Ne vérifie pas que db.query() est appelé avec les bons arguments. Vérifie que GET /users/1 renvoie un 200 avec les bonnes données. Les tests d’intégration résistent aux refactorings, pas les mocks.

Oublier les cas limites. Le body vide, le JSON malformé, les valeurs aux bornes (priority=0, priority=6), les strings très longues. Pydantic protège bien, mais vérifie que tes 422 sont claires.

La base de test qui fuit. Si tu utilises PostgreSQL pour les tests, assure-toi de rollback entre les tests. SQLite en mémoire est plus simple — mais attention, certains comportements PostgreSQL (JSONB, constraints) ne sont pas identiques en SQLite.

⚠️ N’utilise pas create_all() en production. C’est parfait pour les tests, mais en prod tu veux Alembic pour les migrations — il gère les modifications de schéma sans perdre les données existantes.

Ce qu’on retient

🎯 Les tests et la doc ne sont pas du bonus — c’est ce qui fait la différence entre un prototype et une API de production.

Les essentiels :

  • TestClient + SQLite — teste sans serveur HTTP ni PostgreSQL
  • dependency_overrides — injecte la base de test sans toucher au code prod
  • 4 tests par ressource minimum — create OK, doublon, not found, validation
  • Fixtures pour l’authauth_headers réutilisable dans tous les tests
  • pytest --cov — vise 80% de couverture sur les endpoints
  • Swagger automatique — tags, responses, descriptions enrichissent la doc sans effort

Tu maîtrises maintenant les fondamentaux de FastAPI — des routes aux tests en passant par la base de données. La prochaine étape : le déploiement avec Docker, Uvicorn et un reverse proxy pour mettre ton API en production. 🚀

🖥️ Pratique sur ton propre serveur

Pour suivre Apprendre FastAPI en conditions réelles, tu as besoin d'un VPS. DigitalOcean offre 200$ de crédit gratuit pour démarrer.

Obtenir 200$

Articles liés