Nieudany test powinien od razu pokazać, co nie działa, a nie zmuszać do przekopywania się przez logi. W pytest służy do tego prosta asercja oparta na słowie kluczowym assert, która pozwala sprawdzać wartości, typy, wyjątki i wyniki obliczeń. Pokażę, jak pisać takie warunki, kiedy używać pytest.raises() i pytest.approx(), a także jak unikać testów, które przechodzą przypadkiem.
Dobra asercja szybko pokazuje, co poszło nie tak
-
assertporównuje wynik działania z oczekiwaniem i zatrzymuje test przy niezgodności. - Pytest pokazuje wartości użyte w porównaniu, dlatego komunikaty błędów są zwykle bardziej użyteczne niż w standardowym
unittest. - Do wyjątków służy
pytest.raises(), a do liczb zmiennoprzecinkowychpytest.approx(). - Najlepsze asercje sprawdzają jedno zachowanie i mają komunikat zrozumiały bez czytania implementacji.
- W testach parametryzowanych ten sam warunek można bezpiecznie uruchomić dla wielu zestawów danych.
Jak działa pytest assert w teście Pythonowym
Najprostsza asercja ma postać assert warunek. Jeżeli warunek zwróci True, test trwa dalej. Gdy otrzymamy False, pytest zgłasza błąd AssertionError i przerywa wykonanie danej funkcji testowej.
def test_suma():
wynik = 2 + 3
assert wynik == 5
W praktyce nie sprawdzam wyłącznie samego wyniku. Zapisuję także dane pośrednie, gdy poprawia to czytelność testu. Taki styl ułatwia znalezienie przyczyny błędu, szczególnie gdy funkcja wykonuje kilka operacji.
def test_cena_po_rabacie():
cena = 100
rabat = 0.2
wynik = cena * (1 - rabat)
assert wynik == 80
Siłą pytest jest introspekcja asercji. Przy porównaniu assert wynik == 80 narzędzie pokaże między innymi rzeczywistą i oczekiwaną wartość. Nie trzeba więc za każdym razem pisać rozbudowanych metod w rodzaju assertEqual().
Najczęściej używane porównania
| Cel | Przykład | Kiedy użyć |
|---|---|---|
| Równość | assert status == 200 |
Gdy wynik powinien być dokładnie określony |
| Nierówność | assert token != "" |
Gdy sprawdzasz, że wartość nie jest pusta lub niepoprawna |
| Przynależność | assert "admin" in role_list |
Gdy element ma znajdować się w kolekcji |
| Typ | assert isinstance(user_id, int) |
Gdy kontrakt funkcji obejmuje konkretny typ danych |
| Wartość logiczna | assert response.ok |
Gdy obiekt udostępnia czytelny warunek logiczny |
Unikam jednak sprawdzania zbyt wielu rzeczy w jednym wierszu. Asercja assert user.name and user.email and user.active może szybko wskazać, że coś jest nie tak, ale nie powie dokładnie co. Trzy krótsze warunki bywają mniej efektowne, za to znacznie szybsze w diagnozie.
Jak pisać czytelne asercje dla wartości i kolekcji
Najlepiej, gdy test opisuje zachowanie systemu, a nie szczegóły jego implementacji. Zamiast sprawdzać każdy wewnętrzny krok funkcji, porównuję jej publiczny rezultat z jasno określonym oczekiwaniem.
def test_normalizuje_nazwe():
assert normalizuj_nazwe(" Ala ") == "Ala"
Przy słownikach i listach pytest potrafi pokazać różnice między strukturami. To szczególnie wygodne przy testowaniu odpowiedzi API, konfiguracji albo danych zapisanych w pliku JSON.
def test_dane_uzytkownika():
dane = pobierz_uzytkownika()
assert dane == {
"id": 7,
"name": "Ala",
"active": True,
}
Jeżeli test nie powinien wymagać identycznej kolejności elementów, nie porównuję list bezpośrednio. W takim przypadku lepszy może być zbiór, sortowanie albo osobne sprawdzenie liczby elementów. Forma porównania powinna odpowiadać regułom biznesowym, a nie przypadkowemu formatowi danych.
def test_uprawnienia():
uprawnienia = pobierz_uprawnienia()
assert set(uprawnienia) == {"read", "write"}
Komunikaty pomagają, ale nie zastępują dobrego warunku
Do asercji można dodać własny komunikat. Jest przydatny wtedy, gdy sama różnica wartości nie wyjaśnia intencji testu.
assert wiek >= 18, "Użytkownik musi być pełnoletni"
Nie dodaję komunikatów mechanicznie do każdej asercji. Pytest i tak dostarcza szczegółowego raportu, a sztuczne teksty w rodzaju „warunek nie został spełniony” niczego nie wnoszą. Komunikat powinien wskazywać regułę lub kontekst biznesowy, którego nie widać w samym kodzie.
Wyjątki, liczby zmiennoprzecinkowe i wartości specjalne
Nie każdy test polega na sprawdzeniu wyniku funkcji. Czasem poprawne zachowanie oznacza odrzucenie danych i zgłoszenie konkretnego wyjątku. Wtedy nie używam zwykłego assert, bo musiałbym ręcznie przechwytywać błąd.
import pytest
def test_odrzuca_pusta_nazwe():
with pytest.raises(ValueError):
utworz_produkt("")
Można także sprawdzić treść komunikatu. To ma sens, gdy komunikat jest częścią kontraktu, na przykład jest wyświetlany użytkownikowi albo wykorzystywany przez inne narzędzie.
def test_wskazuje_bledny_format():
with pytest.raises(ValueError, match="Nieprawidłowy format"):
parsuj_date("2026/99/99")
Nie przywiązuję się jednak do całego komunikatu, jeśli może się on zmienić bez zmiany zachowania aplikacji. W takim przypadku wyrażenie regularne w parametrze match powinno sprawdzać tylko istotny fragment.
Dlaczego pytest.approx() jest lepsze od dokładnego porównania
Liczby zmiennoprzecinkowe często nie są przechowywane w pamięci dokładnie. Dlatego wynik działania 0.1 + 0.2 może różnić się od matematycznego 0.3 o bardzo małą wartość.
import pytest
def test_oblicza_procent():
wynik = 0.1 + 0.2
assert wynik == pytest.approx(0.3)
Domyślna tolerancja sprawdza, czy różnica mieści się w rozsądnym zakresie. Gdy test dotyczy finansów, pomiarów albo obliczeń naukowych, ustawiam tolerancję jawnie, ponieważ akceptowalny błąd wynika z domeny, a nie z samego pytest.
def test_pomiar_temperatury():
temperatura = odczytaj_temperature()
assert temperatura == pytest.approx(21.5, abs=0.1)
To rozwiązanie działa także dla sekwencji, słowników i tablic liczbowych. Nie stosowałbym go jednak do zwykłych tekstów, identyfikatorów ani kwot, które powinny być porównywane dokładnie, najlepiej jako typ Decimal.
Parametryzacja pozwala sprawdzać wiele przypadków jednym testem
Jeśli ta sama funkcja ma działać dla kilku danych wejściowych, nie kopiuję całej funkcji testowej. Dekorator pytest.mark.parametrize uruchamia ją wielokrotnie, a każda iteracja otrzymuje własne wartości.
import pytest
@pytest.mark.parametrize(
"tekst, oczekiwany",
[
("ala", "Ala"),
(" kot", "Kot"),
("DOM ", "Dom"),
],
)
def test_normalizuje_tekst(tekst, oczekiwany):
assert normalizuj(tekst) == oczekiwany
Parametryzacja jest szczególnie cenna przy walidacji danych brzegowych. W jednym miejscu widać, jakie przypadki uznajemy za poprawne, a raport pytest potrafi wskazać konkretny zestaw argumentów, dla którego test się nie powiódł.
@pytest.mark.parametrize(
"wiek, poprawne",
[
(0, False),
(17, False),
(18, True),
(120, True),
(121, False),
],
)
def test_waliduje_wiek(wiek, poprawne):
assert czy_poprawny_wiek(wiek) is poprawne
Moim zdaniem lepiej dodać pięć sensownych przypadków niż kilkadziesiąt losowych. Największą wartość mają dane graniczne, puste, nietypowe i takie, które wcześniej powodowały błąd. Parametryzacja nie zastępuje przemyślanego scenariusza, tylko ułatwia jego zapis.
Typowe błędy przy stosowaniu asercji
Jednym z częstszych problemów jest asercja, która zawsze przechodzi, bo sprawdza nie to, co trzeba. Przykładem jest porównanie funkcji z samą sobą zamiast wywołanie jej i porównanie wyniku.
# Błąd
assert oblicz_rabat == oblicz_rabat
# Poprawnie
assert oblicz_rabat(100, 20) == 80
Inny problem to testowanie wartości typu truthy zamiast konkretnego rezultatu. Warunek assert lista sprawdza tylko, czy lista nie jest pusta. Jeśli interesuje mnie jej zawartość, powinienem porównać elementy albo długość.
# Tylko sprawdzenie, że coś istnieje
assert produkty
# Sprawdzenie konkretnego wyniku
assert produkty == ["chleb", "mleko"]
Uważam też za ryzykowne używanie assert w kodzie produkcyjnym do walidacji danych. Python może uruchomić program z opcją optymalizacji, przy której asercje zostaną pominięte. W aplikacji używaj do tego wyjątków, warunków i jawnej obsługi błędów, a assert zostaw głównie testom oraz niezmiennikom programistycznym.
Przeczytaj również: Chrome XPath - Pisz stabilne locatory. Testuj skutecznie.
Jak rozdzielać warunki w jednym teście
Test powinien mieć jeden główny powód niepowodzenia. Jeżeli sprawdzam jednocześnie status odpowiedzi, nagłówki i piętnaście pól JSON, później trudno ustalić, która reguła faktycznie się zepsuła.
def test_odpowiedz_api():
odpowiedz = pobierz_dane()
assert odpowiedz.status_code == 200
assert odpowiedz.json()["active"] is True
assert "id" in odpowiedz.json()
To nie oznacza, że każda funkcja testowa musi zawierać tylko jedną linijkę. Chodzi raczej o spójny scenariusz i krótkie, niezależne sprawdzenia. Gdy test rośnie do kilkudziesięciu asercji, zwykle jest to sygnał, że warto rozbić go na mniejsze przypadki.
Jak wykorzystać asercje w automatyzacji testów
Sama asercja nie automatyzuje procesu, jeśli testy nie są regularnie uruchamiane. W projekcie warto połączyć pytest z poleceniem wykonywanym lokalnie oraz w CI, czyli środowisku, które uruchamia testy po zmianie kodu.
pytest -q
Parametr -q ogranicza ilość komunikatów, ale nie ukrywa błędów. Przy diagnozie konkretnego przypadku przydają się także uruchomienie jednego pliku, jednej funkcji lub zatrzymanie po pierwszym niepowodzeniu.
pytest tests/test_users.py::test_odrzuca_pusta_nazwe -q
pytest -x
W pipeline nie traktuję zielonego wyniku jako dowodu, że aplikacja jest bezbłędna. Oznacza on tylko, że sprawdzone scenariusze przeszły. Jakość zależy od tego, czy asercje obejmują krytyczne ścieżki, przypadki brzegowe i błędne dane.
Najlepszy zestaw testów łączy proste porównania, sprawdzanie wyjątków, tolerancję dla obliczeń zmiennoprzecinkowych i parametryzację. Do tego dochodzi stabilne środowisko, powtarzalne dane oraz raport, który pozwala szybko znaleźć przyczynę problemu.
Mała checklista przed dodaniem kolejnej asercji
- Czy sprawdzam rezultat widoczny dla użytkownika lub innego modułu, a nie przypadkowy szczegół implementacji?
- Czy warunek jasno mówi, jakie zachowanie uznaję za poprawne?
- Czy przy liczbach zmiennoprzecinkowych potrzebuję tolerancji?
- Czy wyjątek jest częścią oczekiwanego zachowania i powinien być przechwycony przez
pytest.raises()? - Czy dane obejmują przypadek pusty, graniczny i błędny?
- Czy po niepowodzeniu będę wiedzieć, co konkretnie wymaga poprawy?
Dobrze napisana asercja jest mała, ale nie przypadkowa. Gdy opisuje jedno ważne zachowanie, korzysta z właściwego rodzaju porównania i daje czytelny raport, pytest staje się praktycznym narzędziem codziennej automatyzacji, a nie tylko kolejnym poleceniem uruchamianym przed wdrożeniem.