Zum Inhalt

Testen

🌐 Übersetzung durch KI und Menschen

Diese Übersetzung wurde von KI erstellt, angeleitet von Menschen. 🤝

Sie könnte Fehler enthalten, etwa Missverständnisse des ursprünglichen Sinns oder unnatürliche Formulierungen, usw. 🤖

Sie können diese Übersetzung verbessern, indem Sie uns helfen, die KI-LLM besser anzuleiten.

Englische Version

Dank Starlette ist das Testen von FastAPI-Anwendungen einfach und macht Spaß.

Es basiert auf HTTPX, welches wiederum auf der Grundlage von Requests konzipiert wurde, es ist also sehr vertraut und intuitiv.

Damit können Sie pytest direkt mit FastAPI verwenden.

TestClient verwenden

Hinweis

Um TestClient zu verwenden, installieren Sie zunächst httpx.

Erstellen Sie eine virtuelle Umgebung, aktivieren Sie sie und installieren Sie es dann, z. B.:

$ pipinstallhttpx

Importieren Sie TestClient.

Erstellen Sie einen TestClient, indem Sie ihm Ihre FastAPI-Anwendung übergeben.

Erstellen Sie Funktionen mit einem Namen, der mit test_ beginnt (das ist eine Standard-pytest-Konvention).

Verwenden Sie das TestClient-Objekt auf die gleiche Weise wie httpx.

Schreiben Sie einfache assert-Anweisungen mit den Standard-Python-Ausdrücken, die Sie überprüfen müssen (wiederum, Standard-pytest).

fromfastapiimport FastAPI
fromfastapi.testclientimport TestClient
app = FastAPI()
@app.get("/")
async defread_main():
 return {"msg": "Hello World"}
client = TestClient(app)
deftest_read_main():
 response = client.get("/")
 assert response.status_code == 200
 assert response.json() == {"msg": "Hello World"}

Tipp

Beachten Sie, dass die Testfunktionen normal def und nicht async def sind.

Und die Aufrufe an den Client sind ebenfalls normale Aufrufe, die nicht await verwenden.

Dadurch können Sie pytest ohne Komplikationen direkt nutzen.

Technische Details

Sie könnten auch from starlette.testclient import TestClient verwenden.

FastAPI stellt denselben starlette.testclient auch via fastapi.testclient bereit, als Annehmlichkeit für Sie, den Entwickler. Es kommt aber tatsächlich direkt von Starlette.

Tipp

Wenn Sie in Ihren Tests neben dem Senden von Requests an Ihre FastAPI-Anwendung auch async-Funktionen aufrufen möchten (z. B. asynchrone Datenbankfunktionen), werfen Sie einen Blick auf die Async-Tests im Handbuch für fortgeschrittene Benutzer.

Tests separieren

In einer echten Anwendung würden Sie Ihre Tests wahrscheinlich in einer anderen Datei haben.

Und Ihre FastAPI-Anwendung könnte auch aus mehreren Dateien/Modulen, usw. bestehen.

FastAPI-Anwendungsdatei

Nehmen wir an, Sie haben eine Dateistruktur wie in Größere Anwendungen beschrieben:

.
├── app
│  ├── __init__.py
│  └── main.py

In der Datei main.py haben Sie Ihre FastAPI-Anwendung:

fromfastapiimport FastAPI
app = FastAPI()
@app.get("/")
async defread_main():
 return {"msg": "Hello World"}

Testdatei

Dann könnten Sie eine Datei test_main.py mit Ihren Tests haben. Sie könnte sich im selben Python-Package befinden (dasselbe Verzeichnis mit einer __init__.py-Datei):

.
├── app
│  ├── __init__.py
│  ├── main.py
│  └── test_main.py

Da sich diese Datei im selben Package befindet, können Sie relative Importe verwenden, um das Objekt app aus dem main-Modul (main.py) zu importieren:

fromfastapi.testclientimport TestClient
from.mainimport app
client = TestClient(app)
deftest_read_main():
 response = client.get("/")
 assert response.status_code == 200
 assert response.json() == {"msg": "Hello World"}

... und haben den Code für die Tests wie zuvor.

Testen: erweitertes Beispiel

Nun erweitern wir dieses Beispiel und fügen weitere Details hinzu, um zu sehen, wie verschiedene Teile getestet werden.

Erweiterte FastAPI-Anwendungsdatei

Fahren wir mit der gleichen Dateistruktur wie zuvor fort:

.
├── app
│  ├── __init__.py
│  ├── main.py
│  └── test_main.py

Nehmen wir an, dass die Datei main.py mit Ihrer FastAPI-Anwendung jetzt einige andere Pfadoperationen hat.

Sie verfügt über eine GET-Operation, die einen Fehler zurückgeben könnte.

Sie verfügt über eine POST-Operation, die mehrere Fehler zurückgeben könnte.

Beide Pfadoperationen erfordern einen X-Token-Header.

fromtypingimport Annotated
fromfastapiimport FastAPI, Header, HTTPException
frompydanticimport BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
 "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
 "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
classItem(BaseModel):
 id: str
 title: str
 description: str | None = None
@app.get("/items/{item_id}", response_model=Item)
async defread_main(item_id: str, x_token: Annotated[str, Header()]):
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item_id not in fake_db:
 raise HTTPException(status_code=404, detail="Item not found")
 return fake_db[item_id]
@app.post("/items/")
async defcreate_item(item: Item, x_token: Annotated[str, Header()]) -> Item:
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item.id in fake_db:
 raise HTTPException(status_code=409, detail="Item already exists")
 fake_db[item.id] = item.model_dump()
 return item
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

fromfastapiimport FastAPI, Header, HTTPException
frompydanticimport BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
 "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
 "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
classItem(BaseModel):
 id: str
 title: str
 description: str | None = None
@app.get("/items/{item_id}", response_model=Item)
async defread_main(item_id: str, x_token: str = Header()):
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item_id not in fake_db:
 raise HTTPException(status_code=404, detail="Item not found")
 return fake_db[item_id]
@app.post("/items/")
async defcreate_item(item: Item, x_token: str = Header()) -> Item:
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item.id in fake_db:
 raise HTTPException(status_code=409, detail="Item already exists")
 fake_db[item.id] = item.model_dump()
 return item

Erweiterte Testdatei

Anschließend könnten Sie test_main.py mit den erweiterten Tests aktualisieren:

fromfastapi.testclientimport TestClient
from.mainimport app
client = TestClient(app)
deftest_read_item():
 response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 200
 assert response.json() == {
 "id": "foo",
 "title": "Foo",
 "description": "There goes my hero",
 }
deftest_read_item_bad_token():
 response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_read_nonexistent_item():
 response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 404
 assert response.json() == {"detail": "Item not found"}
deftest_create_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
 )
 assert response.status_code == 200
 assert response.json() == {
 "id": "foobar",
 "title": "Foo Bar",
 "description": "The Foo Barters",
 }
deftest_create_item_bad_token():
 response = client.post(
 "/items/",
 headers={"X-Token": "hailhydra"},
 json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
 )
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_create_existing_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={
 "id": "foo",
 "title": "The Foo ID Stealers",
 "description": "There goes my stealer",
 },
 )
 assert response.status_code == 409
 assert response.json() == {"detail": "Item already exists"}
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

fromfastapi.testclientimport TestClient
from.mainimport app
client = TestClient(app)
deftest_read_item():
 response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 200
 assert response.json() == {
 "id": "foo",
 "title": "Foo",
 "description": "There goes my hero",
 }
deftest_read_item_bad_token():
 response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_read_nonexistent_item():
 response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 404
 assert response.json() == {"detail": "Item not found"}
deftest_create_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
 )
 assert response.status_code == 200
 assert response.json() == {
 "id": "foobar",
 "title": "Foo Bar",
 "description": "The Foo Barters",
 }
deftest_create_item_bad_token():
 response = client.post(
 "/items/",
 headers={"X-Token": "hailhydra"},
 json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
 )
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_create_existing_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={
 "id": "foo",
 "title": "The Foo ID Stealers",
 "description": "There goes my stealer",
 },
 )
 assert response.status_code == 409
 assert response.json() == {"detail": "Item already exists"}

Immer wenn der Client Informationen im Request übergeben soll und Sie nicht wissen, wie, können Sie danach suchen (googeln), wie es mit httpx gemacht wird, oder sogar, wie es mit requests gemacht wird, da das Design von HTTPX auf dem Design von Requests basiert.

Dann machen Sie in Ihren Tests einfach das gleiche.

Z. B.:

  • Um einen Pfad- oder Query-Parameter zu übergeben, fügen Sie ihn der URL selbst hinzu.
  • Um einen JSON-Body zu übergeben, übergeben Sie ein Python-Objekt (z. B. ein dict) an den Parameter json.
  • Wenn Sie Formulardaten anstelle von JSON senden müssen, verwenden Sie stattdessen den data-Parameter.
  • Um Header zu übergeben, verwenden Sie ein dict im headers-Parameter.
  • Für Cookies ein dict im cookies-Parameter.

Weitere Informationen zum Übergeben von Daten an das Backend (mithilfe von httpx oder dem TestClient) finden Sie in der HTTPX-Dokumentation.

Hinweis

Beachten Sie, dass der TestClient Daten empfängt, die nach JSON konvertiert werden können, keine Pydantic-Modelle.

Wenn Sie ein Pydantic-Modell in Ihrem Test haben und dessen Daten während des Testens an die Anwendung senden möchten, können Sie den jsonable_encoder verwenden, der in JSON-kompatibler Encoder beschrieben wird.

Tests ausführen

Danach müssen Sie nur noch pytest installieren.

Erstellen Sie eine virtuelle Umgebung, aktivieren Sie sie und installieren Sie es dann, z. B.:

$ pipinstallpytest
---> 100%

Es erkennt die Dateien und Tests automatisch, führt sie aus und berichtet Ihnen die Ergebnisse.

Führen Sie die Tests aus, mit:

$ pytest
================ test session starts ================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 6 items
---> 100%
test_main.py <span style="color: green; white-space: pre;">...... [100%]</span>
<span style="color: green;">================= 1 passed in 0.03s =================</span>

AltStyle によって変換されたページ (->オリジナル) /