0%

Wat een framework voor je doet · oefening

Test de app in hetzelfde proces

Je app heeft twee padoperaties en nog geen manier om een verzoek te ontvangen. In les 10.4 gaf je een httpx-client een eigen transport, httpx.MockTransport, waarvan de handler elk verzoek in hetzelfde proces beantwoordde, zonder server en zonder . levert hetzelfde idee, gericht op een app. Open tests/test_app.py; de eerste test gebruikt het al:

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) is een httpx-client waarvan het transport de app rechtstreeks aanroept. get("/books") bouwt een httpx.Request, het transport geeft dat aan FastAPI alsof het via een verbinding was binnengekomen, en het antwoord van FastAPI komt terug als een httpx.Response met de status_code, headers en json() die je in hoofdstuk 10 gebruikte. De basis- van de client is http://testserver, een naam die niemand opzoekt: er wordt geen verbinding gemaakt, geen poort geopend, en niets verlaat het Python-proces. Daarom kan dit hoofdstuk de app testen zonder server.

De gevallen van hoofdstuk 5, opnieuw gevraagd

Hier zijn de leesgevallen uit hoofdstuk 4 tot en met 6, gevraagd aan je server uit hoofdstuk 9 in reading_server.py en aan de FastAPI-app met de uit les 1, op de versies die deze cursus gebruikt (die in requirements.txt):

VerzoekJe serverDe FastAPI-app
GET /books200200
GET /books/2200200
GET /books/99404, {"error": "no book has that id"}404, {"detail":"no such book"}
GET /nowhere404404, {"detail":"Not Found"}
HEAD /books200 met de content-length van GET, geen body405, allow: GET
DELETE /books405, allow: GET, HEAD, POST405, allow: GET
GET /books/abc404422
GET /books/301 naar /books307 naar http://testserver/books

De eerste vier rijen antwoorden zoals eerst, met een fractie van de code. De laatste vier zijn beslissingen die FastAPI voor je heeft genomen:

  • HEAD. De @app.get-regel registreert een route alleen voor GET, en FastAPI voegt er geen HEAD aan toe, dus de app antwoordt 405. In les 4.5 liet je je eigen server HEAD beantwoorden zoals GET.

  • De allow-. Twee routes delen het pad /books, een voor GET en een voor POST. Als geen van beide de accepteert, noemt de 405 van FastAPI alleen de methodes van de eerste route waarvan het pad overeenkwam. RFC 9110 zegt dat Allow de methodes noemt die de resource ondersteunt, dus deze lijst laat POST weg.

  • /books/abc. De annotatie book_id: int uit les 1: FastAPI kan van abc geen int maken, dus het antwoordt 422 voordat je functie draait.

  • De slash aan het eind. FastAPI verwijst /books/ door naar /books met een 307, naar een absolute URL die is opgebouwd uit de host die het verzoek noemde. Een 307 behoudt de methode, net als een 308, maar zegt dat de verhuizing tijdelijk is. TestClient volgt , tenzij je follow_redirects=False meegeeft, dus een gewone get("/books/") laat je de 200 aan het eind zien.

De tabel splitst zich dus bij één lijn. Zoek waar elk verzoek op weg naar binnen stopt:

FastAPI antwoordt zelf met 404, 405, 422 en 307, en alleen de overige verzoeken bereiken je functies, die met 200 of 404 antwoorden.

GET /nowhere, HEAD /books, DELETE /books, GET /books/abc en GET /books/ bereiken je code nooit: FastAPI beantwoordt ze zelf. Alleen GET /books, GET /books/2 en GET /books/99 roepen je functies aan.

DELETE /books krijgt 405 met allow: GET. Wat is daar mis mee?

HEAD /books krijgt 405 van de app. Wat klopt?

Tests die een beslissing vastleggen

De rijen voor /books, /books/2 en /books/99 testen je functies. De andere testen FastAPI: zelfs de 404 voor /nowhere is van FastAPI, omdat geen enkele route overeenkomt. Een test die beweert dat HEAD /books 405 krijgt, zegt niet dat 405 juist is. Hij legt vast wat deze app vandaag doet, op deze versies. Als een latere FastAPI HEAD gaat beantwoorden, faalt die test, en de mislukking vertelt je dat het framework onder je is veranderd, en dat wil je weten voordat een client het merkt. De tweede test van les 2 legt twee van deze antwoorden vast.

Bouw de client in elke test

fresh_client() reset STATE en bouwt een TestClient voor reading_app.app telkens als een test het aanroept. Dat is belangrijk voor Check my work: dat draait je tests tegen andere versies van de app, elk met één fout, en zet elke versie op zijn plaats door reading_app.app te vervangen. reading_app.get_book vervangen zou niets veranderen, omdat de route de functie houdt die hij kreeg toen de @app.get-regel draaide. Een test die fresh_client() aanroept, zoekt reading_app.app op wanneer hij draait, dus hij treft de versie die op dat moment op zijn plaats staat. Een client die één keer bovenaan het bestand is gebouwd, zoals client = TestClient(reading_app.app), houdt de oorspronkelijke app vast, en from reading_app import app doet dat ook.

Waar stuurt TestClient(app).get("/books") het verzoek naartoe?

Eén waarschuwing

Draai de tests in het tabblad Shell:

python -m pytest -q

De samenvattingsregel eindigt met 1 warning, en de samenvatting van waarschuwingen erboven heeft één item waarvan de laatste regel zo eindigt:

DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.

Die komt van anyio, de bibliotheek die Starlette, de toolkit waarop FastAPI is gebouwd, gebruikt om de async-code van de app te draaien. De melding gaat over een naam die Starlette intern gebruikt. Ze komt van het importeren van de testclient, en ze verandert geen enkel resultaat.

Je opdracht

Vervang de twee plaatshouders van les 2 in tests/test_app.py, en bouw in elk de client met fresh_client():

  1. test_unknown_book_is_404: GET /books/99 antwoordt 404. De voorbeeldstate heeft alleen de boeken 1 en 2.

  2. test_choices_fastapi_makes: HEAD /books antwoordt 405, en GET /books/abc antwoordt 422.

Controleer statussen met assert; je mag ook allow of je detail controleren. Als je in één test meerdere verzoeken wilt proberen, is pytest.mark.parametrize uit Python in Practice een nette vorm, en gewone asserts in één functie werken ook.

Draai python -m pytest -q opnieuw. Het eindigt met 2 failed, 3 passed, 1 warning: de twee mislukkingen zijn de plaatshouders van les 5, assert False, "not written yet", die op les 5 wachten, en Check my work laat ze weg. Druk daarna op Check my work. Les 3 geeft de body van het verzoek aan het model.

Opdracht

Vervang de twee plaatshouders van les 2 in tests/test_app.py door tests die verzoeken sturen via fresh_client():

  1. test_unknown_book_is_404: GET /books/99 antwoordt 404. De voorbeeldstate bevat alleen de boeken 1 en 2.

  2. test_choices_fastapi_makes: HEAD /books antwoordt 405, en GET /books/abc antwoordt 422. beantwoordt ze allebei zelf.

Bouw de client binnen elke test met fresh_client(). Houd import reading_app en roep alles aan via de : geen regel from reading_app import ..., geen TestClient(...) bovenaan het bestand, en geen naam bovenaan die reading_app.app bewaart. De tests hebben alleen fastapi.testclient, eventueel pytest, en reading_app nodig; importeer geen module socket, http, urllib, uvicorn, requests of httpx. Laat de twee plaatshouders van les 5 staan voor les 5.

In de draait python -m pytest -q de tests. Als beide geschreven zijn, eindigt het met 2 failed, 3 passed, 1 warning; de twee mislukkingen zijn de plaatshouders van les 5.

Druk daarna op Check my work. Het leest de bestanden in tests/ en draait je tests daarna in hetzelfde proces, zonder de twee tests van les 5, drie keer: tegen je eigen app, waar minstens drie tests moeten slagen en geen enkele mag falen, en daarna tegen twee versies van de app die uit je zijn gebouwd, met elk één fout. In de eerste antwoordt get_book met 200 en null voor een onbekend id; in de tweede is het padsegment als tekst gedeclareerd, zodat /books/abc 404 krijgt in plaats van FastAPI’s 422. Tegen elk ervan moet minstens één test falen. Een run die langer duurt dan 3 seconden wordt gestopt en gemeld. list_books en get_book uit les 1 moeten eerst geschreven zijn. Niets uit je terminal wordt beoordeeld.