Kurs Python · Moduł 6: Async i FastAPI
Testing - weryfikacja stabilności
W tej lekcji4
Witaj! Darwin z testing dla FastAPI!
Wyobraź sobie, że poprawiasz jedną linijkę w endpointcie logowania, a po wdrożeniu okazuje się, że przestała działać lista gatunków. Nikt tego nie sprawdził, bo "przecież zmiana była mała". Testy automatyczne łapią takie wpadki w kilka sekund, zanim zobaczy je ktokolwiek poza Tobą.
Testing to krytyczna część produkcyjnych aplikacji. FastAPI ma świetne wsparcie dla pytest!
Analogia Safari: Testing to jak weryfikacja sprzętu przed safari - sprawdzamy, czy GPS działa, czy pojazd jest sprawny, czy radio łapie! Lepiej wykryć problemy przed wycieczką!
Instalacja
Instalujemy trzy pakiety: pytest uruchamia testy, httpx to klient HTTP, na którym FastAPI opiera swojego klienta testowego, a pytest-asyncio pozwala pisać testy jako funkcje async def:
1pip install pytest httpx pytest-asyncioNic więcej nie trzeba konfigurować. pytest sam znajduje pliki o nazwach test_*.py lub *_test.py i uruchamia w nich funkcje, których nazwy zaczynają się od test.
Podstawowy test FastAPI
TestClient symuluje żądania HTTP do aplikacji bez uruchamiania serwera - nie potrzebujesz uvicorna ani wolnego portu. Tworzysz go raz, przekazując obiekt app, i pierwszy test może sprawdzić trasę główną:
1# test_main.py
2from fastapi.testclient import TestClient
3from main import app
4
5client = TestClient(app)
6
7def test_root():
8 response = client.get("/")
9 assert response.status_code == 200
10 assert response.json() == {"message": "Hello Safari"}Test to zwykła funkcja def, nawet jeśli endpointy w aplikacji są async - TestClient sam zajmuje się pętlą zdarzeń. Słowo assert sprawdza warunek, a gdy ten nie jest spełniony, pytest pokaże obie porównywane wartości, więc od razu widzisz, co poszło nie tak.
Kolejne testy sprawdzają listę gatunków i pojedynczy rekord. Oprócz kodu statusu warto zajrzeć do treści odpowiedzi:
1def test_list_species():
2 response = client.get("/species")
3 assert response.status_code == 200
4 assert isinstance(response.json(), list)
5
6def test_get_species():
7 response = client.get("/species/1")
8 assert response.status_code == 200
9 data = response.json()
10 assert data["id"] == 1
11 assert "name" in dataresponse.json() zamienia odpowiedź na słownik lub listę Pythona. Nie porównujemy całego rekordu, tylko sprawdzamy to, co naprawdę ważne: typ wyniku, id i obecność pola name.
Test tworzenia gatunku wysyła ciało żądania parametrem json:
1def test_create_species():
2 response = client.post("/species", json={
3 "name": "Gepard",
4 "scientific_name": "Acinonyx jubatus",
5 "population": 7100,
6 "habitat": "savanna"
7 })
8 assert response.status_code == 201
9 assert response.json()["name"] == "Gepard"Uwaga na pułapkę: FastAPI domyślnie zwraca dla POST kod 200. Ten test przejdzie tylko wtedy, gdy endpoint deklaruje @app.post("/species", status_code=201). Kod 201 Created lepiej opisuje utworzenie zasobu, więc polecam dopisać ten parametr w aplikacji, zamiast osłabiać test.
Ostatni test sprawdza ścieżkę błędu:
1def test_species_not_found():
2 response = client.get("/species/999")
3 assert response.status_code == 404Testowanie przypadków negatywnych jest równie ważne jak pozytywnych. Gatunek o id 999 nie istnieje, więc oczekujemy 404, a nie 500 ani pustej odpowiedzi.
Uruchom testy:
Wystarczy jedno polecenie w katalogu projektu:
1pytestKażda kropka w wyniku to test, który przeszedł, a litera F oznacza porażkę ze szczegółowym opisem poniżej. Flaga -v wypisze nazwę każdego testu.
Async tests
Czasem sam test musi być asynchroniczny, na przykład gdy wywołujesz w nim funkcje async z bazą danych. Wtedy zamiast TestClient używamy AsyncClient z httpx, a dekorator @pytest.mark.asyncio z pakietu pytest-asyncio mówi pytestowi, że funkcję trzeba uruchomić w pętli zdarzeń:
1import pytest
2from httpx import ASGITransport, AsyncClient
3from main import app
4
5@pytest.mark.asyncio
6async def test_list_species_async():
7 async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
8 response = await client.get("/species")
9 assert response.status_code == 200ASGITransport łączy klienta bezpośrednio z aplikacją, bez sieci, a base_url może być dowolny, bo żadne żądanie nie wychodzi na zewnątrz. W starszym kodzie, także w ćwiczeniach, spotkasz skrót AsyncClient(app=app). httpx oznaczył go jako przestarzały w wersji 0.27, a w wersji 0.28 usunął - dziś kończy się błędem TypeError. Dokumentacja FastAPI używa zamiast pytest-asyncio dekoratora @pytest.mark.anyio - oba podejścia działają, wybierz jedno i trzymaj się go w całym projekcie.
Dobre nawyki
Testy powyżej zakładają, że w bazie jest gatunek o id 1. To kruche założenie: wynik zależy od tego, co akurat leży w bazie. W praktyce testy korzystają z osobnej bazy testowej, a FastAPI pozwala podmienić zależność get_db przez app.dependency_overrides, więc endpointy dostają sesję testową bez zmiany ich kodu. Pisz testy razem z funkcją, a nie "kiedyś" - im dłużej czekasz, tym trudniej je dopisać.
Następna lekcja: Production deployment! Zielone testy to przepustka, bez której aplikacja nie powinna wyjechać do chmury.
Pamiętaj: test to przegląd sprzętu przed wyjazdem - dziesięć minut w obozie oszczędza godzin na sawannie z zepsutym radiem.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Do czego służy pytest-asyncio?
2. Co zapewnia TestClient w FastAPI?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Ułóż asynchroniczny test endpoint:
- Klikanie w kolejności
Ułóż asercję sprawdzającą status code:
- Edytor kodu
Stwórz async test sprawdzający, że GET /species zwraca status 200 i listę.