Apprenez à tester efficacement vos applications FastAPI en utilisant TestClient. Couvrez les meilleures pratiques, les techniques avancées et des exemples concrets pour des API robustes et fiables.
MaĂźtriser les tests FastAPI : Un guide complet de TestClient
FastAPI est devenu un framework de premier plan pour la construction d'API haute performance avec Python. Sa rapiditĂ©, sa facilitĂ© d'utilisation et la validation automatique des donnĂ©es en font un favori parmi les dĂ©veloppeurs du monde entier. Cependant, une API bien construite n'est aussi bonne que ses tests. Des tests approfondis garantissent que votre API fonctionne comme prĂ©vu, reste stable sous pression et peut ĂȘtre dĂ©ployĂ©e en production en toute confiance. Ce guide complet se concentre sur l'utilisation du TestClient de FastAPI pour tester efficacement vos points de terminaison d'API.
Pourquoi les tests sont-ils importants pour les applications FastAPI ?
Les tests sont une étape cruciale du cycle de vie du développement logiciel. Cela vous aide à :
- Identifier les bugs tÎt : Détecter les erreurs avant qu'elles n'atteignent la production, ce qui permet d'économiser du temps et des ressources.
- Assurer la qualité du code : Promouvoir un code bien structuré et maintenable.
- Prévenir les régressions : Garantir que les nouvelles modifications ne cassent pas les fonctionnalités existantes.
- Améliorer la fiabilité de l'API : Renforcer la confiance dans la stabilité et les performances de l'API.
- Faciliter la collaboration : Fournir une documentation claire du comportement attendu pour les autres développeurs.
Présentation du TestClient de FastAPI
FastAPI fournit un TestClient intĂ©grĂ© qui simplifie le processus de test de vos points de terminaison d'API. Le TestClient agit comme un client lĂ©ger qui peut envoyer des requĂȘtes Ă votre API sans dĂ©marrer un serveur Ă part entiĂšre. Cela rend les tests considĂ©rablement plus rapides et plus pratiques.
Principales caractéristiques de TestClient :
- Simule les requĂȘtes HTTP : Vous permet d'envoyer des requĂȘtes GET, POST, PUT, DELETE et d'autres requĂȘtes HTTP Ă votre API.
- GĂšre la sĂ©rialisation des donnĂ©es : SĂ©rialise automatiquement les donnĂ©es de requĂȘte (par exemple, les charges utiles JSON) et dĂ©sĂ©rialise les donnĂ©es de rĂ©ponse.
- Fournit des mĂ©thodes d'assertion : Offre des mĂ©thodes pratiques pour vĂ©rifier le code d'Ă©tat, les en-tĂȘtes et le contenu des rĂ©ponses.
- Prend en charge les tests asynchrones : Fonctionne de maniĂšre transparente avec la nature asynchrone de FastAPI.
- S'intĂšgre aux frameworks de test : S'intĂšgre facilement aux frameworks de test Python populaires comme pytest et unittest.
Configuration de votre environnement de test
Avant de commencer les tests, vous devez configurer votre environnement de test. Cela implique généralement l'installation des dépendances nécessaires et la configuration de votre framework de test.
Installation
Tout d'abord, assurez-vous d'avoir FastAPI et pytest installés. Vous pouvez les installer en utilisant pip :
pip install fastapi pytest httpx
httpx est un client HTTP que FastAPI utilise en coulisses. Bien que TestClient fasse partie de FastAPI, le fait d'avoir httpx installé garantit également des tests fluides. Certains tutoriels mentionnent également requests, cependant, httpx est plus aligné sur la nature asynchrone de FastAPI.
Exemple d'application FastAPI
Créons une application FastAPI simple que nous pouvons utiliser pour les tests :
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
Enregistrez ce code sous le nom de main.py. Cette application définit trois points de terminaison :
/: Un point de terminaison GET simple qui renvoie un message "Hello World"./items/{item_id}: Un point de terminaison GET qui renvoie un élément en fonction de son ID./items/: Un point de terminaison POST qui crée un nouvel élément.
Ăcrire votre premier test
Maintenant que vous avez une application FastAPI, vous pouvez commencer Ă Ă©crire des tests en utilisant le TestClient. CrĂ©ez un nouveau fichier nommĂ© test_main.py dans le mĂȘme rĂ©pertoire que main.py.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
Dans ce test :
- Nous importons
TestClientet l'instanceappde FastAPI. - Nous créons une instance de
TestClient, en passantapp. - Nous définissons une fonction de test
test_read_root. - à l'intérieur de la fonction de test, nous utilisons
client.get("/")pour envoyer une requĂȘte GET au point de terminaison racine. - Nous affirmons que le code d'Ă©tat de la rĂ©ponse est 200 (OK).
- Nous affirmons que le JSON de la rĂ©ponse est Ă©gal Ă
{"message": "Hello World"}.
Exécuter vos tests avec pytest
Pour exécuter vos tests, ouvrez simplement un terminal dans le répertoire contenant votre fichier test_main.py et exécutez la commande suivante :
pytest
pytest découvrira et exécutera automatiquement tous les tests de votre projet. Vous devriez voir une sortie similaire à ceci :
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ===============================
Tester différentes méthodes HTTP
Le TestClient prend en charge toutes les méthodes HTTP standard, y compris GET, POST, PUT, DELETE et PATCH. Voyons comment tester chacune de ces méthodes.
Tester les requĂȘtes GET
Nous avons dĂ©jĂ vu un exemple de test d'une requĂȘte GET dans la section prĂ©cĂ©dente. Voici un autre exemple, testant le point de terminaison /items/{item_id} :
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Ce test envoie une requĂȘte GET Ă /items/1 avec un paramĂštre de requĂȘte q=test. Il affirme ensuite que le code d'Ă©tat de la rĂ©ponse est 200 et que le JSON de la rĂ©ponse contient les donnĂ©es attendues.
Tester les requĂȘtes POST
Pour tester une requĂȘte POST, vous devez envoyer des donnĂ©es dans le corps de la requĂȘte. Le TestClient sĂ©rialise automatiquement les donnĂ©es en JSON.
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
Dans ce test :
- Nous créons un dictionnaire
item_datacontenant les données du nouvel élément. - Nous utilisons
client.post("/items/", json=item_data)pour envoyer une requĂȘte POST au point de terminaison/items/, en passantitem_datacomme charge utile JSON. - Nous affirmons que le code d'Ă©tat de la rĂ©ponse est 200 et que le JSON de la rĂ©ponse correspond Ă
item_data.
Tester les requĂȘtes PUT, DELETE et PATCH
Tester les requĂȘtes PUT, DELETE et PATCH est similaire au test des requĂȘtes POST. Vous utilisez simplement les mĂ©thodes correspondantes sur le TestClient :
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Add assertions for the expected response
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
N'oubliez pas d'ajouter des assertions pour vérifier que les réponses sont comme prévu.
Techniques de test avancées
Le TestClient offre plusieurs fonctionnalités avancées qui peuvent vous aider à écrire des tests plus complets et efficaces.
Tester avec des dépendances
Le systÚme d'injection de dépendances de FastAPI vous permet d'injecter facilement des dépendances dans vos points de terminaison d'API. Lors des tests, vous pouvez remplacer ces dépendances pour fournir des implémentations simulées ou spécifiques aux tests.
Par exemple, supposons que votre application dépende d'une connexion à la base de données. Vous pouvez remplacer la dépendance de la base de données dans vos tests pour utiliser une base de données en mémoire :
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Database Configuration
DATABASE_URL = "sqlite:///./test.db" # In-memory database for testing
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Define User Model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# FastAPI App
app = FastAPI()
# Dependency to get the database session
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpoint to create a user
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Override the database dependency for testing
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# First, ensure the tables are created, which may not happen by default
Base.metadata.create_all(bind=engine) # important: create the tables in the test db
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Clean up the override after the test if needed
app.dependency_overrides = {}
Cet exemple remplace la dĂ©pendance get_db par une fonction spĂ©cifique au test qui renvoie une session Ă une base de donnĂ©es SQLite en mĂ©moire. Important : La crĂ©ation des mĂ©tadonnĂ©es doit ĂȘtre explicitement invoquĂ©e pour que la base de donnĂ©es de test fonctionne correctement. Si la table n'est pas créée, des erreurs liĂ©es Ă l'absence de tables se produiront.
Tester le code asynchrone
FastAPI est conçu pour ĂȘtre asynchrone, vous devrez donc souvent tester le code asynchrone. Le TestClient prend en charge les tests asynchrones de maniĂšre transparente.
Pour tester un point de terminaison asynchrone, définissez simplement votre fonction de test comme async :
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simulate some async operation
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Needed to be compatible with pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Remarque : Vous devez installer pytest-asyncio pour utiliser @pytest.mark.asyncio : pip install pytest-asyncio. Vous devez Ă©galement vous assurer que asyncio.get_event_loop() est configurĂ© si vous utilisez d'anciennes versions de pytest. Si vous utilisez pytest version 8 ou plus rĂ©cente, cela peut ne pas ĂȘtre nĂ©cessaire.
Tester le téléchargement de fichiers
FastAPI facilite la gestion des tĂ©lĂ©chargements de fichiers. Pour tester les tĂ©lĂ©chargements de fichiers, vous pouvez utiliser le paramĂštre files des mĂ©thodes de requĂȘte du TestClient.
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
Dans ce test, nous crĂ©ons un fichier factice en utilisant io.BytesIO et nous le transmettons au paramĂštre files. Le paramĂštre files accepte une liste de tuples, oĂč chaque tuple contient le nom du champ, le nom du fichier et le contenu du fichier. Le type de contenu est important pour une gestion prĂ©cise par le serveur.
Tester la gestion des erreurs
Il est important de tester la façon dont votre API gĂšre les erreurs. Vous pouvez utiliser le TestClient pour envoyer des requĂȘtes non valides et vĂ©rifier que l'API renvoie les rĂ©ponses d'erreur correctes.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
Ce test envoie une requĂȘte GET Ă /items/101, qui lĂšve une HTTPException avec un code d'Ă©tat de 400. Le test affirme que le code d'Ă©tat de la rĂ©ponse est 400 et que le JSON de la rĂ©ponse contient le message d'erreur attendu.
Tester les fonctionnalités de sécurité
Si votre API utilise l'authentification ou l'autorisation, vous devrez Ă©galement tester ces fonctionnalitĂ©s de sĂ©curitĂ©. Le TestClient vous permet de dĂ©finir des en-tĂȘtes et des cookies pour simuler des requĂȘtes authentifiĂ©es.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Security
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simulate authentication
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# First, get a token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Then, use the token to access the protected route
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # corrected format.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
Dans cet exemple, nous testons le point de terminaison de connexion, puis nous utilisons le token obtenu pour accĂ©der Ă une route protĂ©gĂ©e. Le paramĂštre headers des mĂ©thodes de requĂȘte du TestClient vous permet de dĂ©finir des en-tĂȘtes personnalisĂ©s, y compris l'en-tĂȘte Authorization pour les jetons bearer.
Meilleures pratiques pour les tests FastAPI
Voici quelques bonnes pratiques Ă suivre lors du test de vos applications FastAPI :
- Ăcrivez des tests complets : Visez une couverture de test Ă©levĂ©e pour vous assurer que toutes les parties de votre API sont testĂ©es de maniĂšre approfondie.
- Utilisez des noms de test descriptifs : Assurez-vous que les noms de vos tests indiquent clairement ce que le test vérifie.
- Suivez le modÚle Arrange-Act-Assert : Organisez vos tests en trois phases distinctes : Arrange (préparer les données de test), Act (effectuer l'action testée) et Assert (vérifier les résultats).
- Utilisez des objets mock : Simulez les dépendances externes pour isoler vos tests et éviter de vous fier aux systÚmes externes.
- Testez les cas limites : Testez votre API avec des entrées non valides ou inattendues pour vous assurer qu'elle gÚre les erreurs avec élégance.
- Exécutez les tests fréquemment : Intégrez les tests dans votre flux de travail de développement pour détecter les bugs tÎt et souvent.
- IntĂ©grez avec CI/CD : Automatisez vos tests dans votre pipeline CI/CD pour vous assurer que toutes les modifications de code sont testĂ©es de maniĂšre approfondie avant d'ĂȘtre dĂ©ployĂ©es en production. Des outils comme Jenkins, GitLab CI, GitHub Actions ou CircleCI peuvent ĂȘtre utilisĂ©s pour y parvenir.
Exemple : Test d'internationalisation (i18n)
Lors du développement d'API pour un public mondial, l'internationalisation (i18n) est essentielle. Tester l'i18n implique de vérifier que votre API prend correctement en charge plusieurs langues et régions. Voici un exemple de la façon dont vous pouvez tester l'i18n dans une application FastAPI :
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "ÂĄHola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "ÂĄHola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
Cet exemple dĂ©finit l'en-tĂȘte Accept-Language pour spĂ©cifier la langue souhaitĂ©e. L'API renvoie le message d'accueil dans la langue spĂ©cifiĂ©e. Les tests garantissent que l'API gĂšre correctement les diffĂ©rentes prĂ©fĂ©rences linguistiques. Si l'en-tĂȘte Accept-Language est absent, la langue par dĂ©faut "en" est utilisĂ©e.
Conclusion
Les tests sont une partie essentielle de la crĂ©ation d'applications FastAPI robustes et fiables. Le TestClient fournit un moyen simple et pratique de tester vos points de terminaison d'API. En suivant les meilleures pratiques dĂ©crites dans ce guide, vous pouvez Ă©crire des tests complets qui garantissent la qualitĂ© et la stabilitĂ© de vos API. Des requĂȘtes de base aux techniques avancĂ©es telles que l'injection de dĂ©pendances et les tests asynchrones, le TestClient vous permet de crĂ©er un code bien testĂ© et maintenable. Adoptez les tests comme une partie essentielle de votre flux de travail de dĂ©veloppement, et vous crĂ©erez des API Ă la fois puissantes et fiables pour les utilisateurs du monde entier. N'oubliez pas l'importance de l'intĂ©gration CI/CD pour automatiser les tests et assurer une assurance qualitĂ© continue.