pytest parametrize w praktyce - testy bez duplikacji kodu

Porównanie: bez pytest parametrize 5 funkcji testowych i 25 linii kodu, z pytest parametrize 1 funkcja testowa i 8 linii kodu.

Napisano przez

Eryk Pawlak

Opublikowano

11 paź 2026

Spis treści

Jedna funkcja potrafi zamienić krótki test w całą serię sensownych scenariuszy bez kopiowania kodu. W tym artykule pokazuję, jak działa pytest parametrize, jak tworzyć przypadki dla wielu danych wejściowych, oznaczać je czytelnymi identyfikatorami, testować wyjątki oraz łączyć parametryzację z fixture’ami.

Jedna definicja testu może obsłużyć dziesiątki scenariuszy

  • Parametryzacja uruchamia tę samą funkcję testową dla wielu zestawów danych.
  • Podstawą jest dekorator @pytest.mark.parametrize.
  • Argument ids poprawia czytelność raportów i ułatwia znalezienie błędnego przypadku.
  • Do wyjątków, pominięć i oczekiwanych awarii służy pytest.param.
  • Fixture’y można parametryzować bezpośrednio albo przez indirect=True.

Porównanie: bez pytest parametrize 5 funkcji testowych i 25 linii kodu, z pytest parametrize 1 funkcja testowa i 8 linii kodu.

Jak działa pytest parametrize w praktyce

Parametryzacja pozwala opisać test raz, a potem uruchomić go z wieloma wartościami. Zamiast pisać trzy niemal identyczne funkcje dla liczb dodatnich, zera i wartości ujemnych, przekazuję dane wejściowe oraz oczekiwany wynik do dekoratora.

import pytest

@pytest.mark.parametrize(
    "a, b, expected",
    [
        (2, 3, 5),
        (10, 5, 15),
        (-1, 4, 3),
    ],
)
def test_addition(a, b, expected):
    assert a + b == expected

Ten test zostanie wykonany trzy razy. Każdy wiersz listy dostarcza osobny zestaw argumentów, ale sama logika sprawdzenia pozostaje wspólna. To właśnie robi największą różnicę w utrzymaniu testów, bo poprawkę w asercji wprowadzam tylko w jednym miejscu.

Najważniejsza zasada jest prosta. Nazwy podane w pierwszym argumencie dekoratora muszą odpowiadać wartościom w każdym zestawie danych i występować w tej samej kolejności. Przy dwóch argumentach każdy przypadek powinien więc zawierać dokładnie dwie wartości.

Jeden argument i dane brzegowe

Najprostszy wariant sprawdza zachowanie funkcji dla pojedynczych danych. Dobrze nadaje się do walidacji, konwersji typów, filtrowania albo funkcji obsługujących graniczne wartości.

@pytest.mark.parametrize("value", [0, 1, 100, -1])
def test_is_integer(value):
    assert isinstance(value, int)

Nie ograniczam się przy tym do przypadków, które powinny działać poprawnie. Zero, pusta wartość, liczba ujemna i bardzo duża liczba często ujawniają błędy, których nie widać przy testowaniu wyłącznie typowego scenariusza.

Wiele argumentów bez powielania funkcji

Gdy funkcja zwraca wynik zależny od kilku danych, wszystkie można przekazać w jednym wierszu. Przykład z rabatem pokazuje, że dane testowe mogą być czytelne nawet wtedy, gdy scenariusz ma więcej parametrów.

@pytest.mark.parametrize(
    "price, discount, expected",
    [
        (100, 0, 100),
        (100, 10, 90),
        (200, 25, 150),
    ],
)
def test_discount(price, discount, expected):
    assert calculate_discount(price, discount) == expected

W praktyce zwracam uwagę na to, by lista przypadków nie stała się przypadkowym zbiorem liczb. Każdy wiersz powinien reprezentować konkretny scenariusz biznesowy, a nie tylko zwiększać liczbę testów.

Jak przygotować czytelne przypadki testowe

Sam wynik testu to nie wszystko. Przy kilku lub kilkunastu zestawach danych komunikat w stylu test_discount[100-10-90] szybko przestaje być wygodny. Czytelne nazwy przypadków pomagają od razu zobaczyć, co właściwie się zepsuło.

Identyfikatory przypadków

Do tego służy parametr ids. Podaję listę nazw w tej samej kolejności co dane, dzięki czemu raport pytesta mówi o scenariuszu językiem zrozumiałym dla zespołu.

@pytest.mark.parametrize(
    "username, expected",
    [
        ("anna", True),
        ("", False),
        (" bardzo-dlugi-login ", False),
    ],
    ids=["poprawny_login", "pusty_login", "login_poza_limitem"],
)
def test_username_validation(username, expected):
    assert validate_username(username) is expected

To drobna rzecz, ale bardzo pomaga podczas pracy w CI. Zamiast przeglądać cały test, od razu widzę, że nie przeszedł przypadek pustego loginu albo wartości przekraczającej limit.

Id można też przypisać bezpośrednio do konkretnego zestawu za pomocą pytest.param. Ten sposób jest wygodniejszy, gdy przypadki powstają w różnych miejscach albo nie chcę pilnować osobnej listy nazw.

@pytest.mark.parametrize(
    "role, can_edit",
    [
        pytest.param("admin", True, id="administrator"),
        pytest.param("editor", True, id="redaktor"),
        pytest.param("guest", False, id="gosc"),
    ],
)
def test_edit_permission(role, can_edit):
    assert has_edit_permission(role) is can_edit

Iloczyn kombinacji i kontrola skali

Można użyć kilku dekoratorów parametryzujących. Pytest utworzy wtedy kombinacje wartości, czyli na przykład 3 języki razy 2 typy kont = 6 testów.

@pytest.mark.parametrize("language", ["pl", "en", "de"])
@pytest.mark.parametrize("account_type", ["free", "pro"])
def test_translation(language, account_type):
    assert translation_exists(language, account_type)

To przydatne przy macierzach kompatybilności, ale łatwo przesadzić. Cztery parametry po pięć wartości oznaczają już 625 kombinacji. Gdy liczba przypadków rośnie, wybieram reprezentatywne scenariusze albo rozdzielam testy na mniejsze grupy, zamiast bezrefleksyjnie sprawdzać każdą możliwą kombinację.

Testowanie wyjątków, pominięć i oczekiwanych awarii

Parametryzacja dobrze sprawdza się także wtedy, gdy część danych powinna prowadzić do wyjątku. Najczytelniej rozdzielić scenariusze poprawne od błędnych, ponieważ wtedy każda funkcja testowa ma jednoznaczny cel.

@pytest.mark.parametrize(
    "value",
    ["", None, "abc"],
    ids=["pusty_tekst", "brak_wartosci", "tekst"],
)
def test_parse_id_rejects_invalid_value(value):
    with pytest.raises(ValueError):
        parse_id(value)

Jeżeli różne dane wywołują różne typy wyjątków, przekazuję także oczekiwany wyjątek jako parametr. Dzięki temu test nie musi zawierać dużego łańcucha warunków.

@pytest.mark.parametrize(
    "value, error",
    [
        ("abc", ValueError),
        (None, TypeError),
        ("999999999999999999999", OverflowError),
    ],
)
def test_parse_id_errors(value, error):
    with pytest.raises(error):
        parse_id(value)

Przeczytaj również: Pytest parametrize i fixture - Kiedy co wybrać?

Oznaczanie wyjątkowych przypadków

Czasami jeden przypadek jest znany jako niedziałający na danej platformie albo oczekuje na poprawkę. pytest.param pozwala oznaczyć go jako pominięty lub oczekiwaną porażkę, bez wyłączania całego testu.

@pytest.mark.parametrize(
    "expression, expected",
    [
        ("2 + 2", 4),
        pytest.param(
            "1 / 0",
            None,
            marks=pytest.mark.xfail(reason="Dzielenie przez zero"),
            id="dzielenie_przez_zero",
        ),
    ],
)
def test_expression(expression, expected):
    assert evaluate(expression) == expected

Takie oznaczenie stosuję oszczędnie. Jeśli przypadek jest stale czerwony i nikt nie wie dlaczego, xfail może ukrywać realny problem. Powód powinien być konkretny, a zespół powinien regularnie przeglądać oczekiwane awarie.

Parametryzacja fixture’ów i użycie indirect

Nie zawsze chcę przekazywać do testu surową wartość. Czasem parametr opisuje środowisko, użytkownika albo konfigurację, a dopiero fixture ma zbudować gotowy obiekt. Wtedy parametryzuję fixture za pomocą pola request.param.

@pytest.fixture(params=["sqlite", "memory"])
def database(request):
    db = create_database(request.param)
    yield db
    db.close()

def test_user_can_be_saved(database):
    user = User(name="Anna")
    database.save(user)

    assert database.get_user("Anna") == user

Test zostanie wykonany dla obu baz danych. To dobry wybór, gdy każdy test korzystający z fixture’a powinien sprawdzić wszystkie warianty środowiska. Trzeba jednak pamiętać, że parametryzacja fixture’a mnoży liczbę uruchomień także w innych testach, które z niego korzystają.

Druga opcja to indirect=True. W tym przypadku wartości podane w dekoratorze trafiają najpierw do fixture’a, a nie bezpośrednio do funkcji testowej.

@pytest.fixture
def user(request):
    return create_user(role=request.param)

@pytest.mark.parametrize(
    "user",
    ["admin", "guest"],
    indirect=True,
    ids=["administrator", "gosc"],
)
def test_user_role(user):
    assert user.role in {"admin", "guest"}
Wariant Kiedy go użyć Co otrzymuje test
Bezpośrednia parametryzacja Gdy test sprawdza prostą wartość lub zestaw wartości Surowe dane wejściowe
Parametryzowany fixture Gdy wiele testów ma działać w różnych środowiskach Gotowy obiekt z fixture’a
indirect=True Gdy tylko wybrane testy mają przekazać parametr do fixture’a Obiekt przygotowany przez fixture

Ja wybieram indirect wtedy, gdy parametr opisuje sposób utworzenia obiektu, a nie sam obiekt. Dzięki temu test pozostaje prosty, a przygotowanie złożonego stanu nie trafia do listy danych testowych.

Błędy, które psują sens parametryzacji

Najczęstszy problem to nieprawidłowy kształt danych. Jeśli deklaruję trzy argumenty, każdy przypadek musi zawierać trzy wartości. Błąd pojawi się jeszcze przed właściwym wykonaniem testu, dlatego przy większych listach warto trzymać dane w czytelnej, wieloliniowej formie.

# Błędnie
@pytest.mark.parametrize("a, b, result", [(1, 2)])

# Poprawnie
@pytest.mark.parametrize("a, b, result", [(1, 2, 3)])

Drugą pułapką są obiekty mutowalne, takie jak listy i słowniki. Parametry są przekazywane do testów bez automatycznego kopiowania, więc jeśli test zmieni listę, kolejny przypadek może zobaczyć zmodyfikowany stan. W takich sytuacjach tworzę dane od nowa w fixture’ze albo jawnie wykonuję kopię.

Nie warto również zamieniać parametryzacji w miniaplikację z rozbudowaną logiką warunkową. Gdy test zawiera wiele konstrukcji if zależnych od konkretnego przypadku, traci prostotę. Lepiej rozdzielić scenariusze na dwie funkcje albo przygotować osobne dane i oczekiwane zachowanie.

  • Dodaj ids, gdy przypadków jest więcej niż kilka.
  • Nie mieszaj w jednej funkcji scenariuszy o zupełnie różnych celach.
  • Unikaj gigantycznych tabel danych, które trudno przeglądać w code review.
  • Nie mutuj parametrów bez świadomego przygotowania izolacji.
  • Sprawdzaj, czy pusta lista parametrów nie oznacza przypadkowo pominięcia testu.

Jeśli dane pochodzą z konfiguracji albo z pliku, przydaje się mechanizm pytest_generate_tests. Pozwala on tworzyć parametry dynamicznie, na przykład na podstawie opcji wiersza poleceń. Sięgam po niego dopiero wtedy, gdy zwykła lista lub fixture przestają być czytelne, ponieważ dodatkowa elastyczność zwiększa też trudność diagnozowania błędów.

Jak włączyć parametryzację do codziennej pracy

Najlepszy efekt daje stopniowe podejście. Najpierw wybieram jedną funkcję z powtarzalnymi testami, łączę podobne przypadki w dekoratorze i dodaję nazwy scenariuszy. Dopiero później rozszerzam rozwiązanie na fixture’y, kombinacje parametrów lub dane generowane dynamicznie.

Po uruchomieniu testów pytest pokazuje każdy przypadek osobno. Dzięki temu można uruchomić tylko wybrany scenariusz po jego identyfikatorze albo filtrować testy przez -k. To szczególnie wygodne w debugowaniu, gdy cała grupa ma kilkadziesiąt wariantów, ale błąd dotyczy jednego konkretnego wejścia.

Parametryzacja nie zastępuje dobrze zaprojektowanych testów. Nie naprawi niejasnych asercji, zależności między testami ani niestabilnego środowiska. Daje jednak bardzo praktyczny kompromis między pokryciem wielu danych a utrzymaniem jednej, czytelnej definicji zachowania.

Mały dekorator, duża różnica w jakości testów

Najważniejsza lekcja jest prosta. Używaj parametryzacji wtedy, gdy scenariusze mają tę samą logikę, ale różne dane, oczekiwane wyniki lub warunki wykonania. Zacznij od podstawowego @pytest.mark.parametrize, dodawaj identyfikatory, świadomie kontroluj liczbę kombinacji i sięgaj po fixture’y dopiero wtedy, gdy testy naprawdę tego potrzebują.

W dobrze utrzymanym projekcie każdy przypadek powinien odpowiadać na konkretne pytanie o zachowanie kodu. Jeśli lista parametrów pomaga te pytania zobaczyć, skraca testy i ułatwia diagnozę, spełnia swoją rolę. Jeśli tylko pomnaża liczbę uruchomień bez jasnego celu, czas ją uprościć.

FAQ - Najczęstsze pytania

Użyj dekoratora @pytest.mark.parametrize i przekaż nazwy argumentów oraz listę przypadków. Każdy wiersz danych uruchomi tę samą funkcję testową osobno, na przykład dla liczb dodatnich, zera i wartości ujemnych.

Skorzystaj z parametru ids, podając listę nazw w tej samej kolejności co dane, albo przypisz identyfikator bezpośrednio przez pytest.param(..., id="nazwa"). Dzięki temu raport pokaże scenariusz, taki jak pusty_login, zamiast trudnej do odczytania kombinacji wartości.

Przekaż dane wejściowe i oczekiwany typ wyjątku jako dwa parametry, a następnie użyj with pytest.raises(error). Możesz też oznaczyć konkretny przypadek przez pytest.param jako pominięty lub oczekiwaną porażkę za pomocą xfail.

Parametryzowany fixture sprawdza się, gdy wiele testów powinno działać w różnych środowiskach, na przykład z bazą SQLite i bazą w pamięci. indirect=True wybierz wtedy, gdy tylko wybrane testy mają przekazać parametr do fixture’a, który na jego podstawie tworzy gotowy obiekt.

Kilka dekoratorów tworzy iloczyn kombinacji, więc 3 języki i 2 typy kont dają 6 testów, a 4 parametry po 5 wartości aż 625 kombinacji. Przy dużej skali wybieraj reprezentatywne scenariusze lub dziel testy na mniejsze grupy.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

pytest parametryzacja wyjątki fixture’y ci

Udostępnij artykuł

Eryk Pawlak

Eryk Pawlak

Nazywam się Eryk Pawlak i od 7 lat zajmuję się automatyzacją testów oraz zapewnieniem jakości oprogramowania. Moja przygoda z tym obszarem zaczęła się z fascynacji technologią i chęcią tworzenia produktów, które są nie tylko funkcjonalne, ale także niezawodne. Interesuje mnie, jak dzięki odpowiednim narzędziom i metodom można uprościć skomplikowane procesy testowe, a także jak ważne jest ciągłe doskonalenie jakości w projektach IT. W swojej pracy skupiam się na dostarczaniu rzetelnych, zrozumiałych i aktualnych informacji, które mogą pomóc innym w zrozumieniu wyzwań związanych z QA. Lubię porównywać różne podejścia do automatyzacji, a także analizować trendy w branży, aby móc dzielić się sprawdzonymi rozwiązaniami. Moim celem jest, aby każdy mógł odnaleźć w moich tekstach wartościowe wskazówki, które ułatwią im pracę w obszarze jakości oprogramowania.

Napisz komentarz