Was ein Framework für dich tut · Übung
Die App im selben Prozess testen
Deine App hat zwei Path Operations und noch keinen Weg, eine Anfrage zu empfangen. In Lektion 10.4 hast du einem httpx-Client einen eigenen Transport gegeben, httpx.MockTransport, dessen Handler jede Anfrage im selben Prozess beantwortet hat, ohne Server und ohne tests/test_app.py; sein erster Test benutzt sie schon:
def fresh_client(state=None):
reading_app.reset_state(state)
return TestClient(reading_app.app)
def test_list_books_is_json():
response = fresh_client().get("/books")
assert response.status_code == 200
assert response.json()["books"][0]["title"] == "Kindred"
TestClient(app) ist ein httpx-Client, dessen Transport die App direkt aufruft. get("/books") baut eine httpx.Request, der Transport übergibt sie FastAPI, als wäre sie über eine Verbindung angekommen, und die Antwort von FastAPI kommt als httpx.Response zurück, mit status_code, headers und json(), die du in Kapitel 10 benutzt hast. Die Basis-http://testserver, ein Name, den niemand nachschlägt: Es wird keine Verbindung aufgebaut, kein Port geöffnet, und nichts verlässt den Python-Prozess. Deshalb kann dieses Kapitel die App ohne Server testen.
Die Fälle aus Kapitel 5, noch einmal gestellt
Hier sind die Fälle für Lesezugriffe aus den Kapiteln 4 bis 6, gestellt an deinen Server aus Kapitel 9 in reading_server.py und an die FastAPI-App mit den requirements.txt):
| Anfrage | Dein Server | Die FastAPI-App |
|---|---|---|
GET /books | 200 | 200 |
GET /books/2 | 200 | 200 |
GET /books/99 | 404, {"error": "no book has that id"} | 404, {"detail":"no such book"} |
GET /nowhere | 404 | 404, {"detail":"Not Found"} |
HEAD /books | 200 mit der content-length von GET, ohne Body | 405, allow: GET |
DELETE /books | 405, allow: GET, HEAD, POST | 405, allow: GET |
GET /books/abc | 404 | 422 |
GET /books/ | 301 auf /books | 307 auf http://testserver/books |
Die ersten vier Zeilen antworten wie zuvor, mit einem Bruchteil des Codes. Die letzten vier sind Entscheidungen, die FastAPI für dich getroffen hat:
HEAD. Die@app.get-Zeile registriert eine Route nur für GET, und FastAPI fügt ihr kein HEAD hinzu, also antwortet die App mit 405. In Lektion 4.5 hast du deinen eigenen Server HEAD wie GET beantworten lassen.Die
allow-. Zwei Routen teilen sich den Pfad /books, eine für GET und eine für POST. Wenn keine von beiden dieakzeptiert, nennt die 405 von FastAPI nur die Methoden der ersten Route, deren Pfad gepasst hat. RFC 9110 sagt, dass Allowdie Methoden auflistet, die die Ressource unterstützt, also lässt diese Liste POST aus./books/abc. Die Annotationbook_id: intaus Lektion 1: FastAPI kann ausabckeinen int machen, also antwortet es mit 422, bevor deine Funktion läuft.Der Schrägstrich am Ende. FastAPI leitet
/books/mit einer 307 auf/booksweiter, auf eine absolute URL, die aus dem Host gebaut wird, den die Anfrage genannt hat. Eine 307 behält die Methode bei, wie eine 308, sagt aber, dass der Umzug vorübergehend ist.TestClientfolgt, außer du übergibst follow_redirects=False, also zeigt dir ein einfachesget("/books/")die 200 am Ende.
Die Tabelle teilt sich also an einer Linie. Finde heraus, wo jede Anfrage auf ihrem Weg hinein stehen bleibt:

GET /nowhere, HEAD /books, DELETE /books, GET /books/abc und GET /books/ erreichen deinen Code nie: FastAPI beantwortet sie selbst. Nur GET /books, GET /books/2 und GET /books/99 rufen deine Funktionen auf.
DELETE /books bekommt eine 405 mit allow: GET. Was ist daran falsch?
HEAD /books bekommt von der App eine 405. Was stimmt?
Tests, die eine Entscheidung festhalten
Die Zeilen für /books, /books/2 und /books/99 testen deine Funktionen. Die anderen testen FastAPI: Sogar die 404 für /nowhere stammt von FastAPI, da keine Route passt. Ein Test, der zusichert, dass HEAD /books eine 405 bekommt, behauptet nicht, dass 405 richtig ist. Er hält fest, was diese App heute tut, mit diesen Versionen. Wenn ein späteres FastAPI anfängt, HEAD zu beantworten, schlägt dieser Test fehl, und der Fehlschlag sagt dir, dass sich das Framework unter dir geändert hat. Das solltest du wissen, bevor ein Client es merkt. Der zweite Test aus Lektion 2 hält zwei dieser Antworten fest.
Den Client in jedem Test bauen
fresh_client() setzt STATE zurück und baut jedes Mal, wenn ein Test es aufruft, einen TestClient für reading_app.app. Das ist wichtig für Check my work: Es führt deine Tests gegen andere Versionen der App aus, jede mit einem Fehler, und setzt jede Version ein, indem es reading_app.app ersetzt. reading_app.get_book zu ersetzen würde nichts ändern, weil die Route die Funktion behält, die sie bekommen hat, als die @app.get-Zeile lief. Ein Test, der fresh_client() aufruft, schlägt reading_app.app nach, wenn er läuft, und trifft so auf die Version, die gerade eingesetzt ist. Ein Client, der einmal oben in der Datei gebaut wird, etwa client = TestClient(reading_app.app), behält die ursprüngliche App, und from reading_app import app ebenso.
Wohin sendet TestClient(app).get("/books") die Anfrage?
Eine Warnung
Führe die Tests im Tab Shell aus:
python -m pytest -q
Die Zusammenfassungszeile endet mit 1 warning, und die Zusammenfassung der Warnungen darüber hat einen Eintrag, dessen letzte Zeile so endet:
DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
Sie stammt von anyio, der Bibliothek, mit der Starlette, das Toolkit, auf dem FastAPI aufbaut, den asynchronen Code der App ausführt. Der Hinweis betrifft einen Namen, den Starlette intern benutzt. Er kommt vom Import des Test-Clients und ändert kein Ergebnis.
Deine Aufgabe
Ersetze die beiden Platzhalter von Lektion 2 in tests/test_app.py, wobei jeder seinen Client mit fresh_client() baut:
test_unknown_book_is_404:GET /books/99antwortet mit 404. Der Beispielzustand hat nur die Bücher 1 und 2.test_choices_fastapi_makes:HEAD /booksantwortet mit 405, undGET /books/abcantwortet mit 422.
Sichere allow oder dein detail prüfen. Wenn ein Test mehrere Anfragen ausprobieren soll, ist pytest.mark.parametrize aus Python in der Praxis eine saubere Form, und einfache asserts in einer Funktion funktionieren genauso.
Führe noch einmal python -m pytest -q aus. Es endet mit 2 failed, 3 passed, 1 warning: Die beiden Fehlschläge sind die Platzhalter von Lektion 5, assert False, "not written yet", die auf Lektion 5 warten, und Check my work lässt sie aus. Klicke dann auf Check my work. Lektion 3 übergibt den Body der Anfrage an das Modell.
Aufgabe
Ersetze die beiden Platzhalter von Lektion 2 in tests/test_app.py durch Tests, die Anfragen über fresh_client() senden:
test_unknown_book_is_404:GET /books/99antwortet mit 404. Der Beispielzustand enthält nur die Bücher 1 und 2.test_choices_fastapi_makes:HEAD /booksantwortet mit 405, undGET /books/abcantwortet mit 422.beantwortet beide selbst.
Baue den Client in jedem Test mit fresh_client(). Behalte import reading_app und rufe es über das from reading_app import ..., kein TestClient(...) oben in der Datei und kein Name oben, der reading_app.app speichert. Die Tests brauchen nur fastapi.testclient, pytest, wenn du magst, und reading_app; importiere keine Module socket, http, urllib, uvicorn, requests oder httpx. Lass die beiden Platzhalter von Lektion 5 für Lektion 5 stehen.
Im python -m pytest -q die Tests aus. Wenn beide geschrieben sind, endet es mit 2 failed, 3 passed, 1 warning; die beiden Fehlschläge sind die Platzhalter von Lektion 5.
Klicke dann auf Check my work. Es liest die Dateien in tests/ und führt deine Tests dann in diesem Prozess dreimal aus, ohne die beiden Tests von Lektion 5: gegen deine eigene App, bei der mindestens drei Tests bestehen müssen und keiner fehlschlagen darf, dann gegen zwei Versionen der App, die aus deinen get_book für eine unbekannte ID mit 200 und null; in der zweiten ist das Pfadsegment als Text deklariert, also bekommt /books/abc eine 404 statt der 422 von FastAPI. Gegen jede muss mindestens ein Test fehlschlagen. Ein Lauf, der länger als 3 Sekunden dauert, wird angehalten und gemeldet. list_books und get_book aus Lektion 1 müssen zuerst geschrieben sein. Nichts aus deinem Terminal wird bewertet.