Effektive Tests für FastAPI-Anwendungen mit TestClient. Best Practices, fortgeschrittene Techniken und reale Beispiele für robuste APIs.
FastAPI-Tests meistern: Ein umfassender Leitfaden zu TestClient
FastAPI hat sich zu einem führenden Framework für die Entwicklung performanter APIs mit Python entwickelt. Seine Geschwindigkeit, Benutzerfreundlichkeit und automatische Datenvalidierung machen es bei Entwicklern weltweit beliebt. Eine gut entwickelte API ist jedoch nur so gut wie ihre Tests. Umfassendes Testen stellt sicher, dass Ihre API wie erwartet funktioniert, unter Druck stabil bleibt und vertrauensvoll in die Produktion überführt werden kann. Dieser umfassende Leitfaden konzentriert sich auf die Verwendung von FastAPIs TestClient, um Ihre API-Endpunkte effektiv zu testen.
Warum ist Testen für FastAPI-Anwendungen wichtig?
Testen ist ein entscheidender Schritt im Softwareentwicklungszyklus. Es hilft Ihnen:
- Fehler frühzeitig erkennen: Fehler erfassen, bevor sie in die Produktion gelangen, spart Zeit und Ressourcen.
- Codequalität sicherstellen: Gut strukturierter und wartbarer Code wird gefördert.
- Regressionen verhindern: Sicherstellen, dass neue Änderungen die bestehende Funktionalität nicht beeinträchtigen.
- API-Zuverlässigkeit verbessern: Vertrauen in die Stabilität und Leistung der API aufbauen.
- Zusammenarbeit erleichtern: Klare Dokumentation des erwarteten Verhaltens für andere Entwickler bereitstellen.
FastAPIs TestClient kennenlernen
FastAPI bietet einen integrierten TestClient, der den Prozess des Testens Ihrer API-Endpunkte vereinfacht. Der TestClient fungiert als leichtgewichtiger Client, der Anfragen an Ihre API senden kann, ohne einen vollwertigen Server zu starten. Dies macht das Testen erheblich schneller und bequemer.
Hauptmerkmale von TestClient:
- Simuliert HTTP-Anfragen: Ermöglicht das Senden von GET-, POST-, PUT-, DELETE- und anderen HTTP-Anfragen an Ihre API.
- Verarbeitet Daten-Serialisierung: Serialisiert automatisch Anfragedaten (z. B. JSON-Payloads) und deserialisiert Antwortdaten.
- Bietet Assertions-Methoden: Bietet praktische Methoden zur Überprüfung des Statuscodes, der Header und des Inhalts von Antworten.
- Unterstützt asynchrones Testen: Funktioniert nahtlos mit FastAPIs asynchroner Natur.
- Integration mit Test-Frameworks: Lässt sich leicht in beliebte Python-Test-Frameworks wie pytest und unittest integrieren.
Einrichten Ihrer Testumgebung
Bevor Sie mit dem Testen beginnen, müssen Sie Ihre Testumgebung einrichten. Dies beinhaltet typischerweise die Installation der erforderlichen Abhängigkeiten und die Konfiguration Ihres Test-Frameworks.
Installation
Stellen Sie zunächst sicher, dass Sie FastAPI und pytest installiert haben. Sie können diese mit pip installieren:
pip install fastapi pytest httpx
httpx ist ein HTTP-Client, den FastAPI intern verwendet. Obwohl TestClient Teil von FastAPI ist, sorgt die Installation von httpx ebenfalls für reibungslose Tests. Einige Tutorials erwähnen auch requests, jedoch ist httpx besser auf die asynchrone Natur von FastAPI abgestimmt.
Beispiel-FastAPI-Anwendung
Erstellen wir eine einfache FastAPI-Anwendung, die wir zum Testen verwenden können:
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
Speichern Sie diesen Code als main.py. Diese Anwendung definiert drei Endpunkte:
/: Ein einfacher GET-Endpunkt, der eine "Hello World"-Nachricht zurückgibt./items/{item_id}: Ein GET-Endpunkt, der ein Element anhand seiner ID zurückgibt./items/: Ein POST-Endpunkt, der ein neues Element erstellt.
Ihren ersten Test schreiben
Nachdem Sie nun eine FastAPI-Anwendung haben, können Sie mit dem Schreiben von Tests mit dem TestClient beginnen. Erstellen Sie eine neue Datei namens test_main.py im selben Verzeichnis wie 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"}
In diesem Test:
- Wir importieren
TestClientund die FastAPIapp-Instanz. - Wir erstellen eine Instanz von
TestClientund übergeben dieapp. - Wir definieren eine Testfunktion
test_read_root. - Innerhalb der Testfunktion verwenden wir
client.get("/"), um eine GET-Anfrage an den Root-Endpunkt zu senden. - Wir überprüfen, ob der Antwort-Statuscode 200 (OK) ist.
- Wir überprüfen, ob das Antwort-JSON mit
{"message": "Hello World"}übereinstimmt.
Ihre Tests mit pytest ausführen
Um Ihre Tests auszuführen, öffnen Sie einfach ein Terminal in dem Verzeichnis, das Ihre test_main.py-Datei enthält, und führen Sie den folgenden Befehl aus:
pytest
pytest erkennt und führt automatisch alle Tests in Ihrem Projekt aus. Sie sollten eine Ausgabe ähnlich dieser sehen:
============================= 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 ===============================
Testen verschiedener HTTP-Methoden
Der TestClient unterstützt alle Standard-HTTP-Methoden, einschließlich GET, POST, PUT, DELETE und PATCH. Sehen wir uns an, wie jede dieser Methoden getestet wird.
Testen von GET-Anfragen
Ein Beispiel für das Testen einer GET-Anfrage haben wir bereits im vorherigen Abschnitt gesehen. Hier ist ein weiteres Beispiel, das den /items/{item_id}-Endpunkt testet:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Dieser Test sendet eine GET-Anfrage an /items/1 mit einem Query-Parameter q=test. Anschließend wird überprüft, ob der Antwort-Statuscode 200 ist und ob die Antwort-JSON die erwarteten Daten enthält.
Testen von POST-Anfragen
Um eine POST-Anfrage zu testen, müssen Sie Daten im Anfragkörper senden. Der TestClient serialisiert die Daten automatisch in 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
In diesem Test:
- Wir erstellen ein Dictionary
item_data, das die Daten für das neue Element enthält. - Wir verwenden
client.post("/items/", json=item_data), um eine POST-Anfrage an den Endpunkt/items/zu senden und übergeben dabei dieitem_dataals JSON-Payload. - Wir überprüfen, ob der Antwort-Statuscode 200 ist und ob die Antwort-JSON mit
item_dataübereinstimmt.
Testen von PUT-, DELETE- und PATCH-Anfragen
Das Testen von PUT-, DELETE- und PATCH-Anfragen ähnelt dem Testen von POST-Anfragen. Sie verwenden einfach die entsprechenden Methoden auf dem 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
# Assertionen für die erwartete Antwort hinzufügen
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Assertionen für die erwartete Antwort hinzufügen
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Assertionen für die erwartete Antwort hinzufügen
Denken Sie daran, Assertionen hinzuzufügen, um zu überprüfen, ob die Antworten den Erwartungen entsprechen.
Fortgeschrittene Testtechniken
Der TestClient bietet mehrere fortgeschrittene Funktionen, die Ihnen helfen können, umfassendere und effektivere Tests zu schreiben.
Testen mit Abhängigkeiten
FastAPIs Dependency Injection System ermöglicht es Ihnen, Abhängigkeiten einfach in Ihre API-Endpunkte einzuspeisen. Beim Testen möchten Sie diese Abhängigkeiten möglicherweise überschreiben, um Mock- oder Test-spezifische Implementierungen bereitzustellen.
Angenommen, Ihre Anwendung hängt von einer Datenbankverbindung ab. Sie können die Datenbankabhängigkeit in Ihren Tests überschreiben, um eine In-Memory-Datenbank zu verwenden:
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
# Datenbankkonfiguration
DATABASE_URL = "sqlite:///./test.db" # In-Memory-Datenbank für Tests
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# User-Modell definieren
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()
# Abhängigkeit zum Abrufen der Datenbanksitzung
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpunkt zum Erstellen eines Benutzers
@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)
# Die Datenbankabhängigkeit für Tests überschreiben
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# Zuerst sicherstellen, dass die Tabellen erstellt werden, was standardmäßig möglicherweise nicht geschieht
Base.metadata.create_all(bind=engine) # wichtig: die Tabellen in der Test-DB erstellen
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Bereinigung der Überschreibung nach dem Test, falls erforderlich
app.dependency_overrides = {}
Dieses Beispiel überschreibt die get_db-Abhängigkeit mit einer testspezifischen Funktion, die eine Sitzung für eine In-Memory-SQLite-Datenbank zurückgibt. Wichtig: Die Metadatenerstellung muss explizit aufgerufen werden, damit die Testdatenbank korrekt funktioniert. Das Versäumnis, die Tabelle zu erstellen, führt zu Fehlern im Zusammenhang mit fehlenden Tabellen.
Testen von asynchronem Code
FastAPI ist für die Asynchronität konzipiert, daher müssen Sie oft asynchronen Code testen. Der TestClient unterstützt asynchrone Tests nahtlos.
Um einen asynchronen Endpunkt zu testen, definieren Sie Ihre Testfunktion einfach als async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simulation einer asynchronen Operation
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Erforderlich, um mit pytest-asyncio kompatibel zu sein
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Hinweis: Sie müssen pytest-asyncio installieren, um @pytest.mark.asyncio zu verwenden: pip install pytest-asyncio. Sie müssen auch sicherstellen, dass asyncio.get_event_loop() konfiguriert ist, wenn Sie ältere pytest-Versionen verwenden. Bei Verwendung von pytest Version 8 oder neuer ist dies möglicherweise nicht erforderlich.
Testen von Datei-Uploads
FastAPI erleichtert die Handhabung von Datei-Uploads. Um Datei-Uploads zu testen, können Sie den Parameter files der Request-Methoden des TestClient verwenden.
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"]}
In diesem Test erstellen wir eine Dummy-Datei mit io.BytesIO und übergeben sie an den Parameter files. Der Parameter files akzeptiert eine Liste von Tupeln, wobei jedes Tupel den Feldnamen, den Dateinamen und den Dateiinhalt enthält. Der Inhaltstyp ist für die genaue Verarbeitung durch den Server wichtig.
Testen der Fehlerbehandlung
Es ist wichtig zu testen, wie Ihre API Fehler behandelt. Sie können den TestClient verwenden, um ungültige Anfragen zu senden und zu überprüfen, ob die API die richtigen Fehlerantworten zurückgibt.
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"}
Dieser Test sendet eine GET-Anfrage an /items/101, die eine HTTPException mit einem Statuscode von 400 auslöst. Der Test überprüft, ob der Antwort-Statuscode 400 ist und ob die Antwort-JSON die erwartete Fehlermeldung enthält.
Testen von Sicherheitsfunktionen
Wenn Ihre API Authentifizierung oder Autorisierung verwendet, müssen Sie auch diese Sicherheitsfunktionen testen. Der TestClient ermöglicht es Ihnen, Header und Cookies festzulegen, um authentifizierte Anfragen zu simulieren.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Sicherheit
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Authentifizierung simulieren
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():
# Zuerst einen Token abrufen
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Dann den Token verwenden, um auf die geschützte Route zuzugreifen
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # Korrigiertes Format.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
In diesem Beispiel testen wir den Login-Endpunkt und verwenden dann den erhaltenen Token, um auf eine geschützte Route zuzugreifen. Der Parameter headers der Request-Methoden des TestClient ermöglicht es Ihnen, benutzerdefinierte Header festzulegen, einschließlich des Authorization-Headers für Bearer-Tokens.
Best Practices für FastAPI-Tests
Hier sind einige Best Practices, die Sie beim Testen Ihrer FastAPI-Anwendungen befolgen sollten:
- Schreiben Sie umfassende Tests: Streben Sie eine hohe Testabdeckung an, um sicherzustellen, dass alle Teile Ihrer API gründlich getestet werden.
- Verwenden Sie beschreibende Testnamen: Stellen Sie sicher, dass Ihre Testnamen klar angeben, was der Test überprüft.
- Folgen Sie dem Arrange-Act-Assert-Muster: Organisieren Sie Ihre Tests in drei verschiedene Phasen: Arrange (Testdaten einrichten), Act (die zu testende Aktion ausführen) und Assert (Ergebnisse überprüfen).
- Verwenden Sie Mock-Objekte: Mocken Sie externe Abhängigkeiten, um Ihre Tests zu isolieren und die Abhängigkeit von externen Systemen zu vermeiden.
- Testen Sie Randfälle: Testen Sie Ihre API mit ungültigen oder unerwarteten Eingaben, um sicherzustellen, dass sie Fehler ordnungsgemäß behandelt.
- Führen Sie Tests häufig aus: Integrieren Sie das Testen in Ihren Entwicklungsworkflow, um Fehler frühzeitig und oft zu erkennen.
- Integration mit CI/CD: Automatisieren Sie Ihre Tests in Ihrer CI/CD-Pipeline, um sicherzustellen, dass alle Codeänderungen vor der Bereitstellung in der Produktion gründlich getestet werden. Tools wie Jenkins, GitLab CI, GitHub Actions oder CircleCI können hierfür verwendet werden.
Beispiel: Internationalisierungs-(i18n)-Tests
Bei der Entwicklung von APIs für ein globales Publikum ist Internationalisierung (i18n) unerlässlich. Das Testen von i18n umfasst die Überprüfung, ob Ihre API mehrere Sprachen und Regionen korrekt unterstützt. Hier ist ein Beispiel, wie Sie i18n in einer FastAPI-Anwendung testen können:
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!"}
Dieses Beispiel setzt den Accept-Language-Header, um die gewünschte Sprache anzugeben. Die API gibt die Begrüßung in der angegebenen Sprache zurück. Das Testen stellt sicher, dass die API verschiedene Sprachpräferenzen korrekt verarbeitet. Wenn der Accept-Language-Header fehlt, wird die Standard-Sprache "en" verwendet.
Fazit
Das Testen ist ein wesentlicher Bestandteil der Entwicklung robuster und zuverlässiger FastAPI-Anwendungen. Der TestClient bietet eine einfache und bequeme Möglichkeit, Ihre API-Endpunkte zu testen. Wenn Sie die in diesem Leitfaden beschriebenen Best Practices befolgen, können Sie umfassende Tests schreiben, die die Qualität und Stabilität Ihrer APIs gewährleisten. Von grundlegenden Anfragen bis hin zu fortgeschrittenen Techniken wie Dependency Injection und asynchronem Testen befähigt Sie der TestClient, gut getesteten und wartbaren Code zu erstellen. Nehmen Sie das Testen als Kernbestandteil Ihres Entwicklungs-Workflows an, und Sie werden APIs entwickeln, die sowohl leistungsstark als auch zuverlässig für Benutzer weltweit sind. Denken Sie an die Bedeutung der CI/CD-Integration, um das Testen zu automatisieren und eine kontinuierliche Qualitätssicherung zu gewährleisten.