Lær at teste dine FastAPI-applikationer effektivt med TestClient. Dækker bedste praksis, avancerede teknikker og eksempler fra den virkelige verden for robuste og pålidelige API'er.
Mestring af FastAPI Testing: En Omfattende Guide til TestClient
FastAPI er blevet en førende ramme til at bygge højtydende API'er med Python. Dets hastighed, brugervenlighed og automatiske datavalidering gør det til en favorit blandt udviklere verden over. Men en velbygget API er kun så god som dens tests. Grundig testning sikrer, at din API fungerer som forventet, forbliver stabil under pres og kan implementeres i produktion med tillid. Denne omfattende guide fokuserer på at bruge FastAPI's TestClient til effektivt at teste dine API-endepunkter.
Hvorfor er Testning Vigtigt for FastAPI-applikationer?
Testning er et afgørende skridt i softwareudviklingens livscyklus. Det hjælper dig med at:
- Identificere fejl tidligt: Fang fejl, før de når produktion, hvilket sparer tid og ressourcer.
- Sikre kodekvalitet: Fremme velstruktureret og vedligeholdelig kode.
- Forhindre regressioner: Garantere, at nye ændringer ikke ødelægger eksisterende funktionalitet.
- Forbedre API's pålidelighed: Opbygge tillid til API'ets stabilitet og ydeevne.
- Fremme samarbejde: Give klar dokumentation af forventet adfærd for andre udviklere.
Introduktion til FastAPI's TestClient
FastAPI tilbyder en indbygget TestClient, der forenkler processen med at teste dine API-endepunkter. TestClient fungerer som en letvægtsklient, der kan sende anmodninger til din API uden at starte en fuld server. Dette gør testning betydeligt hurtigere og mere bekvemt.
Nøglefunktioner i TestClient:
- Simulerer HTTP-anmodninger: Giver dig mulighed for at sende GET, POST, PUT, DELETE og andre HTTP-anmodninger til din API.
- Håndterer dataserielisering: Serialiserer automatisk anmodningsdata (f.eks. JSON-payloads) og deserialiserer svardata.
- Tilbyder assertion-metoder: Tilbyder bekvemme metoder til at verificere statuskode, headers og indholdet af svarene.
- Understøtter asynkron testning: Fungerer problemfrit med FastAPI's asynkrone natur.
- Integrerer med testrammer: Integrerer let med populære Python-testrammer som pytest og unittest.
Opsætning af Dit Testmiljø
Før du begynder at teste, skal du opsætte dit testmiljø. Dette involverer typisk installation af de nødvendige afhængigheder og konfiguration af din testramme.
Installation
Først skal du sikre dig, at du har FastAPI og pytest installeret. Du kan installere dem ved hjælp af pip:
pip install fastapi pytest httpx
httpx er en HTTP-klient, som FastAPI bruger under motorhjelmen. Selvom TestClient er en del af FastAPI, sikrer det at have httpx installeret også en problemfri testning. Nogle tutorials nævner også requests, men httpx er mere i tråd med FastAPI's asynkrone natur.
Eksempel på FastAPI-applikation
Lad os oprette en simpel FastAPI-applikation, som vi kan bruge til test:
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
Gem denne kode som main.py. Denne applikation definerer tre endepunkter:
/: Et simpelt GET-endepunkt, der returnerer en "Hello World"-besked./items/{item_id}: Et GET-endepunkt, der returnerer et item baseret på dets ID./items/: Et POST-endepunkt, der opretter et nyt item.
Skriv Din Første Test
Nu hvor du har en FastAPI-applikation, kan du begynde at skrive tests ved hjælp af TestClient. Opret en ny fil med navnet test_main.py i samme mappe som 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"}
I denne test:
- Importerer vi
TestClientog FastAPIapp-instansen. - Vi opretter en instans af
TestClientog senderapp'en med. - Vi definerer en testfunktion
test_read_root. - Indeni testfunktionen bruger vi
client.get("/")til at sende en GET-anmodning til rod-endepunktet. - Vi asserter, at svarets statuskode er 200 (OK).
- Vi asserter, at svarets JSON er lig med
{"message": "Hello World"}.
Kørsel af Dine Tests med pytest
For at køre dine tests skal du blot åbne en terminal i den mappe, der indeholder din test_main.py-fil, og køre følgende kommando:
pytest
pytest vil automatisk finde og køre alle tests i dit projekt. Du skulle se et output, der ligner dette:
============================= 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 ===============================
Test af Forskellige HTTP-metoder
TestClient understøtter alle standard HTTP-metoder, herunder GET, POST, PUT, DELETE og PATCH. Lad os se, hvordan man tester hver af disse metoder.
Test af GET-anmodninger
Vi har allerede set et eksempel på test af en GET-anmodning i det forrige afsnit. Her er endnu et eksempel, der tester /items/{item_id}-endepunktet:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Denne test sender en GET-anmodning til /items/1 med en forespørgselsparameter q=test. Den asserter derefter, at svarets statuskode er 200, og at svarets JSON indeholder de forventede data.
Test af POST-anmodninger
For at teste en POST-anmodning skal du sende data i anmodningens body. TestClient serialiserer automatisk dataene til 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
I denne test:
- Opretter vi en dictionary
item_data, der indeholder dataene for det nye item. - Vi bruger
client.post("/items/", json=item_data)til at sende en POST-anmodning til/items/-endepunktet og senderitem_datasom JSON-payload. - Vi asserter, at svarets statuskode er 200, og at svarets JSON matcher
item_data.
Test af PUT-, DELETE- og PATCH-anmodninger
Test af PUT-, DELETE- og PATCH-anmodninger ligner test af POST-anmodninger. Du bruger simpelthen de tilsvarende metoder på 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
# Tilføj assertions for det forventede svar
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Tilføj assertions for det forventede svar
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Tilføj assertions for det forventede svar
Husk at tilføje assertions for at verificere, at svarene er som forventet.
Avancerede Testteknikker
TestClient tilbyder flere avancerede funktioner, der kan hjælpe dig med at skrive mere omfattende og effektive tests.
Test med Dependencies
FastAPI's dependency injection-system giver dig mulighed for let at injicere afhængigheder i dine API-endepunkter. Under testning vil du måske tilsidesætte disse afhængigheder for at levere mock- eller test-specifikke implementeringer.
For eksempel, antag at din applikation afhænger af en databaseforbindelse. Du kan tilsidesætte databaseafhængigheden i dine tests for at bruge en in-memory database:
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
# Databasekonfiguration
DATABASE_URL = "sqlite:///./test.db" # In-memory database til test
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Definer 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()
# Afhængighed for at få databasesessionen
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endepunkt for at oprette en bruger
@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)
# Tilsidesæt databaseafhængigheden til test
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# Først, sørg for at tabellerne er oprettet, hvilket måske ikke sker som standard
Base.metadata.create_all(bind=engine) # vigtigt: opret tabellerne i testdatabasen
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Ryd op i tilsidesættelsen efter testen, hvis det er nødvendigt
app.dependency_overrides = {}
Dette eksempel tilsidesætter get_db-afhængigheden med en test-specifik funktion, der returnerer en session til en in-memory SQLite-database. Vigtigt: Metadataoprettelsen skal eksplicit kaldes, for at testdatabasen fungerer korrekt. Hvis man undlader at oprette tabellen, vil det føre til fejl relateret til manglende tabeller.
Test af Asynkron Kode
FastAPI er bygget til at være asynkron, så du vil ofte have brug for at teste asynkron kode. TestClient understøtter asynkron testning problemfrit.
For at teste et asynkront endepunkt skal du blot definere din testfunktion som async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simuler en asynkron operation
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Nødvendigt for at være kompatibel med pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Bemærk: Du skal installere pytest-asyncio for at bruge @pytest.mark.asyncio: pip install pytest-asyncio. Du skal også sikre dig, at asyncio.get_event_loop() er konfigureret, hvis du bruger ældre pytest-versioner. Hvis du bruger pytest version 8 eller nyere, er dette muligvis ikke påkrævet.
Test af Filuploads
FastAPI gør det nemt at håndtere filuploads. For at teste filuploads kan du bruge files-parameteren i TestClient's anmodningsmetoder.
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"]}
I denne test opretter vi en dummy-fil ved hjælp af io.BytesIO og sender den til files-parameteren. files-parameteren accepterer en liste af tupler, hvor hver tuple indeholder feltnavnet, filnavnet og filindholdet. Indholdstypen er vigtig for korrekt håndtering af serveren.
Test af Fejlhåndtering
Det er vigtigt at teste, hvordan din API håndterer fejl. Du kan bruge TestClient til at sende ugyldige anmodninger og verificere, at API'en returnerer de korrekte fejlsvar.
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"}
Denne test sender en GET-anmodning til /items/101, hvilket udløser en HTTPException med en statuskode på 400. Testen asserter, at svarets statuskode er 400, og at svarets JSON indeholder den forventede fejlmeddelelse.
Test af Sikkerhedsfunktioner
Hvis din API bruger autentificering eller autorisation, skal du også teste disse sikkerhedsfunktioner. TestClient giver dig mulighed for at indstille headers og cookies for at simulere autentificerede anmodninger.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Sikkerhed
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simuler autentificering
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():
# Først, hent et token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Brug derefter tokenet til at få adgang til den beskyttede rute
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # korrigeret format.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
I dette eksempel tester vi login-endepunktet og bruger derefter det modtagne token til at få adgang til en beskyttet rute. headers-parameteren i TestClient's anmodningsmetoder giver dig mulighed for at indstille brugerdefinerede headers, herunder Authorization-headeren for bearer-tokens.
Bedste Praksis for FastAPI Testning
Her er nogle bedste praksisser, du kan følge, når du tester dine FastAPI-applikationer:
- Skriv omfattende tests: Sigt efter høj testdækning for at sikre, at alle dele af din API er grundigt testet.
- Brug beskrivende testnavne: Sørg for, at dine testnavne tydeligt angiver, hvad testen verificerer.
- Følg Arrange-Act-Assert-mønsteret: Organiser dine tests i tre adskilte faser: Arrange (opsætning af testdata), Act (udfør den handling, der testes), og Assert (verificer resultaterne).
- Brug mock-objekter: Mock eksterne afhængigheder for at isolere dine tests og undgå at være afhængig af eksterne systemer.
- Test kanttilfælde: Test din API med ugyldigt eller uventet input for at sikre, at den håndterer fejl elegant.
- Kør tests ofte: Integrer testning i din udviklingsworkflow for at fange fejl tidligt og ofte.
- Integrer med CI/CD: Automatiser dine tests i din CI/CD-pipeline for at sikre, at alle kodeændringer testes grundigt, før de implementeres i produktion. Værktøjer som Jenkins, GitLab CI, GitHub Actions eller CircleCI kan bruges til at opnå dette.
Eksempel: Internationalisering (i18n) Testning
Når man udvikler API'er til et globalt publikum, er internationalisering (i18n) afgørende. Test af i18n indebærer at verificere, at din API understøtter flere sprog og regioner korrekt. Her er et eksempel på, hvordan du kan teste i18n i en FastAPI-applikation:
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!"}
Dette eksempel indstiller Accept-Language-headeren for at specificere det ønskede sprog. API'en returnerer hilsnen på det specificerede sprog. Testning sikrer, at API'en korrekt håndterer forskellige sprogpræferencer. Hvis Accept-Language-headeren mangler, bruges standardsproget "en".
Konklusion
Testning er en essentiel del af at bygge robuste og pålidelige FastAPI-applikationer. TestClient giver en simpel og bekvem måde at teste dine API-endepunkter på. Ved at følge de bedste praksisser, der er beskrevet i denne guide, kan du skrive omfattende tests, der sikrer kvaliteten og stabiliteten af dine API'er. Fra basale anmodninger til avancerede teknikker som dependency injection og asynkron testning giver TestClient dig mulighed for at skabe veltestet og vedligeholdelig kode. Omfavn testning som en kerne del af din udviklingsworkflow, og du vil bygge API'er, der er både kraftfulde og pålidelige for brugere over hele verden. Husk vigtigheden af CI/CD-integration for at automatisere testning og sikre kontinuerlig kvalitetssikring.