Leer hoe u uw FastAPI-applicaties effectief kunt testen met de TestClient. Behandelt best practices, geavanceerde technieken en praktijkvoorbeelden voor robuuste en betrouwbare API's.
FastAPI Testing onder de knie krijgen: Een complete gids voor TestClient
FastAPI is uitgegroeid tot een toonaangevend framework voor het bouwen van high-performance API's met Python. De snelheid, het gebruiksgemak en de automatische datavalidatie maken het een favoriet onder ontwikkelaars wereldwijd. Echter, een goed gebouwde API is slechts zo goed als de tests ervan. Grondig testen zorgt ervoor dat uw API functioneert zoals verwacht, stabiel blijft onder druk en met vertrouwen in productie kan worden genomen. Deze uitgebreide gids richt zich op het gebruik van FastAPI's TestClient om uw API-endpoints effectief te testen.
Waarom is testen belangrijk voor FastAPI-applicaties?
Testen is een cruciale stap in de levenscyclus van softwareontwikkeling. Het helpt u om:
- Bugs vroegtijdig te identificeren: Spoor fouten op voordat ze de productie bereiken, wat tijd en middelen bespaart.
- Codekwaliteit te waarborgen: Bevorder goed gestructureerde en onderhoudbare code.
- Regressies te voorkomen: Garandeer dat nieuwe wijzigingen de bestaande functionaliteit niet breken.
- API-betrouwbaarheid te verbeteren: Bouw vertrouwen op in de stabiliteit en prestaties van de API.
- Samenwerking te faciliteren: Bied duidelijke documentatie van het verwachte gedrag voor andere ontwikkelaars.
Introductie van FastAPI's TestClient
FastAPI biedt een ingebouwde TestClient die het proces van het testen van uw API-endpoints vereenvoudigt. De TestClient fungeert als een lichtgewicht client die verzoeken naar uw API kan sturen zonder een volledige server te starten. Dit maakt het testen aanzienlijk sneller en gemakkelijker.
Belangrijkste kenmerken van TestClient:
- Simuleert HTTP-verzoeken: Hiermee kunt u GET-, POST-, PUT-, DELETE- en andere HTTP-verzoeken naar uw API sturen.
- Handelt dataserialisatie af: Serialiseert automatisch verzoekgegevens (bijv. JSON-payloads) en deserialiseert responsgegevens.
- Biedt assertiemethoden: Biedt handige methoden voor het verifiëren van de statuscode, headers en inhoud van de responsen.
- Ondersteunt asynchroon testen: Werkt naadloos samen met de asynchrone aard van FastAPI.
- Integreert met testframeworks: Integreert gemakkelijk met populaire Python-testframeworks zoals pytest en unittest.
Uw testomgeving opzetten
Voordat u begint met testen, moet u uw testomgeving opzetten. Dit omvat doorgaans het installeren van de benodigde afhankelijkheden en het configureren van uw testframework.
Installatie
Zorg er eerst voor dat u FastAPI en pytest heeft geïnstalleerd. U kunt ze installeren met pip:
pip install fastapi pytest httpx
httpx is een HTTP-client die FastAPI onder de motorkap gebruikt. Hoewel TestClient deel uitmaakt van FastAPI, zorgt het installeren van httpx voor een soepele testervaring. Sommige tutorials vermelden ook requests, maar httpx sluit beter aan bij de asynchrone aard van FastAPI.
Voorbeeld van een FastAPI-applicatie
Laten we een eenvoudige FastAPI-applicatie maken die we kunnen gebruiken voor het testen:
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
Sla deze code op als main.py. Deze applicatie definieert drie endpoints:
/: Een eenvoudig GET-endpoint dat een "Hello World"-bericht retourneert./items/{item_id}: Een GET-endpoint dat een item retourneert op basis van zijn ID./items/: Een POST-endpoint dat een nieuw item aanmaakt.
Uw eerste test schrijven
Nu u een FastAPI-applicatie heeft, kunt u beginnen met het schrijven van tests met de TestClient. Maak een nieuw bestand genaamd test_main.py in dezelfde map als 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 deze test:
- Importeren we
TestClienten de FastAPIapp-instantie. - We maken een instantie van
TestClient, waarbij we deappmeegeven. - We definiëren een testfunctie
test_read_root. - Binnen de testfunctie gebruiken we
client.get("/")om een GET-verzoek naar het root-endpoint te sturen. - We asserteren dat de responsstatuscode 200 (OK) is.
- We asserteren dat de JSON-respons gelijk is aan
{"message": "Hello World"}.
Uw tests uitvoeren met pytest
Om uw tests uit te voeren, opent u eenvoudig een terminal in de map met uw test_main.py-bestand en voert u de volgende opdracht uit:
pytest
pytest zal automatisch alle tests in uw project ontdekken en uitvoeren. U zou een output moeten zien die hierop lijkt:
============================= 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 ===============================
Verschillende HTTP-methoden testen
De TestClient ondersteunt alle standaard HTTP-methoden, inclusief GET, POST, PUT, DELETE en PATCH. Laten we zien hoe we elk van deze methoden kunnen testen.
GET-verzoeken testen
We hebben al een voorbeeld gezien van het testen van een GET-verzoek in de vorige sectie. Hier is nog een voorbeeld, waarbij het /items/{item_id}-endpoint wordt getest:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Deze test stuurt een GET-verzoek naar /items/1 met een queryparameter q=test. Vervolgens wordt geasserteerd dat de responsstatuscode 200 is en dat de JSON-respons de verwachte gegevens bevat.
POST-verzoeken testen
Om een POST-verzoek te testen, moet u gegevens in de request body sturen. De TestClient serialiseert de gegevens automatisch naar 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 deze test:
- We maken een dictionary
item_dataaan die de gegevens voor het nieuwe item bevat. - We gebruiken
client.post("/items/", json=item_data)om een POST-verzoek naar het/items/-endpoint te sturen, waarbij weitem_dataals JSON-payload meegeven. - We asserteren dat de responsstatuscode 200 is en dat de JSON-respons overeenkomt met de
item_data.
PUT-, DELETE- en PATCH-verzoeken testen
Het testen van PUT-, DELETE- en PATCH-verzoeken is vergelijkbaar met het testen van POST-verzoeken. U gebruikt eenvoudigweg de corresponderende methoden op de 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
# Voeg asserties toe voor de verwachte respons
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Voeg asserties toe voor de verwachte respons
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Voeg asserties toe voor de verwachte respons
Vergeet niet om asserties toe te voegen om te verifiëren dat de responsen zijn zoals verwacht.
Geavanceerde testtechnieken
De TestClient biedt verschillende geavanceerde functies die u kunnen helpen om uitgebreidere en effectievere tests te schrijven.
Testen met dependencies
Het dependency injection-systeem van FastAPI stelt u in staat om gemakkelijk afhankelijkheden in uw API-endpoints te injecteren. Tijdens het testen wilt u deze afhankelijkheden misschien overschrijven om mock- of test-specifieke implementaties te bieden.
Stel bijvoorbeeld dat uw applicatie afhankelijk is van een databaseverbinding. U kunt de database-afhankelijkheid in uw tests overschrijven om een in-memory database te gebruiken:
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 = {}
Dit voorbeeld overschrijft de get_db-dependency met een test-specifieke functie die een sessie naar een in-memory SQLite-database retourneert. Belangrijk: Het aanmaken van de metadata moet expliciet worden aangeroepen om de testdatabase correct te laten functioneren. Als u de tabel niet aanmaakt, leidt dit tot fouten met betrekking tot ontbrekende tabellen.
Asynchrone code testen
FastAPI is gebouwd om asynchroon te zijn, dus u zult vaak asynchrone code moeten testen. De TestClient ondersteunt asynchroon testen naadloos.
Om een asynchroon endpoint te testen, definieert u uw testfunctie eenvoudig als 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"}
Let op: U moet pytest-asyncio installeren om @pytest.mark.asyncio te kunnen gebruiken: pip install pytest-asyncio. U moet er ook voor zorgen dat asyncio.get_event_loop() is geconfigureerd als u oudere pytest-versies gebruikt. Bij gebruik van pytest versie 8 of nieuwer is dit mogelijk niet vereist.
Bestandsuploads testen
FastAPI maakt het gemakkelijk om bestandsuploads af te handelen. Om bestandsuploads te testen, kunt u de files-parameter van de request-methoden van de TestClient gebruiken.
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 deze test maken we een dummybestand met io.BytesIO en geven dit door aan de files-parameter. De files-parameter accepteert een lijst van tuples, waarbij elke tuple de veldnaam, de bestandsnaam en de bestandsinhoud bevat. Het contenttype is belangrijk voor een accurate afhandeling door de server.
Foutafhandeling testen
Het is belangrijk om te testen hoe uw API omgaat met fouten. U kunt de TestClient gebruiken om ongeldige verzoeken te sturen en te verifiëren dat de API de juiste foutresponsen retourneert.
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"}
Deze test stuurt een GET-verzoek naar /items/101, wat een HTTPException met statuscode 400 veroorzaakt. De test asserteert dat de responsstatuscode 400 is en dat de JSON-respons het verwachte foutbericht bevat.
Beveiligingsfuncties testen
Als uw API authenticatie of autorisatie gebruikt, moet u deze beveiligingsfuncties ook testen. De TestClient stelt u in staat om headers en cookies in te stellen om geauthenticeerde verzoeken te simuleren.
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"}
In dit voorbeeld testen we het login-eindpunt en gebruiken we vervolgens het ontvangen token om toegang te krijgen tot een beschermde route. De headers-parameter van de request-methoden van de TestClient stelt u in staat om aangepaste headers in te stellen, inclusief de Authorization-header voor bearer tokens.
Best practices voor het testen van FastAPI
Hier zijn enkele best practices om te volgen bij het testen van uw FastAPI-applicaties:
- Schrijf uitgebreide tests: Streef naar een hoge testdekking om ervoor te zorgen dat alle onderdelen van uw API grondig worden getest.
- Gebruik beschrijvende testnamen: Zorg ervoor dat uw testnamen duidelijk aangeven wat de test verifieert.
- Volg het Arrange-Act-Assert-patroon: Organiseer uw tests in drie verschillende fasen: Arrange (voorbereiden van de testgegevens), Act (uitvoeren van de te testen actie) en Assert (verifiëren van de resultaten).
- Gebruik mock-objecten: Mock externe afhankelijkheden om uw tests te isoleren en te voorkomen dat u afhankelijk bent van externe systemen.
- Test randgevallen: Test uw API met ongeldige of onverwachte invoer om ervoor te zorgen dat deze fouten correct afhandelt.
- Voer tests regelmatig uit: Integreer testen in uw ontwikkelworkflow om bugs vroeg en vaak te vangen.
- Integreer met CI/CD: Automatiseer uw tests in uw CI/CD-pijplijn om ervoor te zorgen dat alle codewijzigingen grondig worden getest voordat ze in productie worden genomen. Tools zoals Jenkins, GitLab CI, GitHub Actions of CircleCI kunnen hiervoor worden gebruikt.
Voorbeeld: Internationalisatie (i18n) testen
Bij het ontwikkelen van API's voor een wereldwijd publiek is internationalisatie (i18n) essentieel. Het testen van i18n omvat het verifiëren dat uw API meerdere talen en regio's correct ondersteunt. Hier is een voorbeeld van hoe u i18n kunt testen in een FastAPI-applicatie:
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!"}
Dit voorbeeld stelt de Accept-Language-header in om de gewenste taal te specificeren. De API retourneert de begroeting in de opgegeven taal. Het testen zorgt ervoor dat de API verschillende taalvoorkeuren correct afhandelt. Als de Accept-Language-header ontbreekt, wordt de standaardtaal "en" gebruikt.
Conclusie
Testen is een essentieel onderdeel van het bouwen van robuuste en betrouwbare FastAPI-applicaties. De TestClient biedt een eenvoudige en handige manier om uw API-endpoints te testen. Door de best practices in deze gids te volgen, kunt u uitgebreide tests schrijven die de kwaliteit en stabiliteit van uw API's waarborgen. Van basisverzoeken tot geavanceerde technieken zoals dependency injection en asynchroon testen, de TestClient stelt u in staat om goed geteste en onderhoudbare code te creëren. Omarm testen als een kernonderdeel van uw ontwikkelworkflow, en u zult API's bouwen die zowel krachtig als betrouwbaar zijn voor gebruikers over de hele wereld. Denk aan het belang van CI/CD-integratie om het testen te automatiseren en continue kwaliteitsborging te garanderen.