Pytest w Pythonie od podstaw - fixture, parametryzacja i CI

Kod Pythona pokazujący tworzenie fabryki pytest fixture. Użyjemy pytest do testowania.

Autor

Eryk Pawlak

Opublikowano

7 lis 2026

Spis treści

Gdy liczba ręcznych sprawdzeń rośnie, a każda zmiana w kodzie może niepostrzeżenie zepsuć inną funkcję, testy automatyczne przestają być dodatkiem i stają się częścią codziennej pracy. Python wraz z biblioteką pytest pozwala szybko budować testy jednostkowe, integracyjne i funkcjonalne, a potem uruchamiać je lokalnie lub w procesie CI. Pokażę, jak zacząć, jak korzystać z fixture, parametryzacji i markerów oraz jak uniknąć typowych błędów.

Najważniejsze informacje o testowaniu w Pythonie z pytest

  • pytest upraszcza tworzenie i uruchamianie testów dzięki zwykłym funkcjom oraz instrukcji assert.
  • Podstawowy zestaw instalacyjny to środowisko wirtualne, pakiet pytest i katalog z plikami testowymi.
  • Fixture przygotowują dane i zasoby, a parametr yzacja pozwala sprawdzić jedną funkcję dla wielu przypadków.
  • Markery pomagają uruchamiać wybrane grupy testów, na przykład szybkie, wolne albo integracyjne.
  • Największą wartość daje nie sama liczba testów, lecz czytelny podział odpowiedzialności i stabilne dane testowe.

Dlaczego pytest tak dobrze sprawdza się w automatyzacji

Pytest to framework do testowania aplikacji napisanych w Pythonie. Jego podstawowa idea jest prosta: piszę funkcję testową, wykonuję kod i sprawdzam wynik za pomocą assert. Nie muszę tworzyć rozbudowanych klas ani dziedziczyć po specjalnych bazach, dlatego pierwszy działający test można przygotować w kilka minut.

Największą przewagą tego narzędzia jest połączenie prostoty z dużymi możliwościami rozbudowy. Na początku wystarczą zwykłe funkcje, ale później mogę dodać fixture, parametryzację, markery, pluginy i własne opcje uruchamiania. Dzięki temu pytest pasuje zarówno do małego projektu, jak i dużej aplikacji rozwijanej przez wiele zespołów.

W praktyce testy najczęściej dzieli się na trzy poziomy. Test jednostkowy sprawdza pojedynczą funkcję lub klasę, test integracyjny kontroluje współpracę kilku elementów, a test funkcjonalny ocenia zachowanie aplikacji z punktu widzenia użytkownika. Pytest może obsługiwać wszystkie te poziomy, ale nie rozwiązuje za mnie problemu złej architektury. Dobry test powinien mieć jasny cel, a nie tylko zwiększać statystykę pokrycia kodu.

Rodzaj testu Co sprawdza Typowy czas
Jednostkowy Pojedynczą funkcję lub klasę Milisekundy
Integracyjny Współpracę modułów, bazy danych lub API Od sekund do minut
Funkcjonalny Cały scenariusz działania aplikacji Zwykle najdłuższy

Moja praktyczna zasada jest prosta: szybkie testy jednostkowe powinny uruchamiać się przy każdej zmianie, natomiast cięższe testy integracyjne można wydzielić markerem i uruchamiać w osobnym etapie. Taki podział skraca informację zwrotną bez rezygnowania z dokładniejszej kontroli.

Instalacja i pierwszy działający test

Najbezpieczniej zacząć od osobnego środowiska wirtualnego. Chroni ono projekt przed konfliktem wersji i sprawia, że zależności testowe są zapisane niezależnie od innych aplikacji na komputerze.

python -m venv .venv
source .venv/bin/activate
python -m pip install pytest

W systemie Windows aktywacja środowiska wygląda inaczej, ale sama instalacja pozostaje taka sama. W projekcie zespołowym pytest powinien trafić do zależności deweloperskich, a nie do przypadkowo instalowanych pakietów globalnych. Powtarzalne środowisko jest równie ważne jak sam kod testu.

Przyjmijmy prostą funkcję w pliku calculator.py:

def add(a, b):
    return a + b

Test może znajdować się w pliku test_calculator.py:

from calculator import add

def test_add_returns_sum():
    assert add(2, 3) == 5

Uruchomienie testów sprowadza się do jednego polecenia:

pytest

Pytest automatycznie wyszukuje pliki o nazwach takich jak test_*.py lub *_test.py, a także funkcje zaczynające się od test_. Gdy potrzebuję większej ilości informacji, używam pytest -v. Opcja verbose pokazuje nazwy poszczególnych testów, co ułatwia znalezienie problemu w większym zestawie.

Można także uruchomić testy przez interpreter:

python -m pytest

Ta forma bywa przydatna, gdy na komputerze znajduje się kilka instalacji Pythona. Mam wtedy większą pewność, że testy korzystają z tego samego interpretera, do którego instalowałem zależności.

Fixture porządkują przygotowanie danych i sprzątanie

W testach często trzeba utworzyć użytkownika, przygotować plik, otworzyć połączenie albo ustawić zmienną środowiskową. Powtarzanie tych czynności w każdej funkcji szybko prowadzi do bałaganu. Fixture pozwala wydzielić przygotowanie zasobów i przekazać gotowy wynik do testu.

import pytest

@pytest.fixture
def user():
    return {
        "name": "Anna",
        "active": True,
    }

def test_active_user(user):
    assert user["active"] is True

Nazwa fixture pojawia się jako argument funkcji testowej. Pytest rozpoznaje ją automatycznie i wstrzykuje przygotowaną wartość. To prosty mechanizm zależności, który dobrze skaluje się wraz z projektem, szczególnie gdy wspólne fixture umieszczę w pliku conftest.py.

Fixture mogą mieć różny zakres życia. Domyślny zakres function tworzy zasób osobno dla każdego testu, natomiast module lub session pozwala współdzielić go dłużej. Szerszy zakres poprawia szybkość, ale może pogorszyć izolację testów, jeśli jeden przypadek zmieni stan używany przez kolejny.

Do zasobów wymagających sprzątania dobrze pasuje instrukcja yield:

import pytest

@pytest.fixture
def temporary_file(tmp_path):
    file_path = tmp_path / "data.txt"
    file_path.write_text("test data", encoding="utf-8")
    yield file_path
    # tutaj można wykonać dodatkowe sprzątanie

def test_file_content(temporary_file):
    assert temporary_file.read_text(encoding="utf-8") == "test data"

Wbudowane tmp_path udostępnia unikalny katalog tymczasowy dla danego testu. To lepsze rozwiązanie niż ręczne tworzenie plików w katalogu projektu, ponieważ ogranicza konflikty i zostawia środowisko robocze w czystym stanie.

Parametryzacja pozwala testować realne warianty

Pojedynczy przypadek testowy rzadko wystarcza. Funkcja może działać poprawnie dla dodatnich liczb, a zawodzić dla zera, wartości ujemnych lub pustych danych. Zamiast kopiować kilka niemal identycznych funkcji, można użyć dekoratora pytest.mark.parametrize.

import pytest

def normalize_name(value):
    return value.strip().title()

@pytest.mark.parametrize(
    "value, expected",
    [
        (" anna ", "Anna"),
        ("JAN", "Jan"),
        (" maria", "Maria"),
    ],
)
def test_normalize_name(value, expected):
    assert normalize_name(value) == expected

Ten test zostanie wykonany trzy razy, z innym zestawem danych. Wynik w terminalu pokaże, który konkretny przypadek się nie powiódł. Dobrze nazwane dane testowe są tu bardzo ważne, bo komunikat o błędzie powinien od razu wskazywać problematyczny wariant.

Parametryzacja sprawdza się również przy wyjątkach:

import pytest

def divide(a, b):
    return a / b

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

Nie testuję w ten sposób samego faktu, że program się zatrzymał. Sprawdzam konkretny typ wyjątku, więc przypadkowy błąd innego rodzaju nie zostanie uznany za poprawne zachowanie. Jeśli komunikat wyjątku jest częścią kontraktu, można dodatkowo zweryfikować jego treść.

Trzeba uważać na mutowalne dane przekazywane do wielu przypadków. Lista lub słownik nie jest automatycznie kopiowany dla każdego wywołania testu. Jeżeli test zmienia taki obiekt, kolejne przypadki mogą otrzymać już zmodyfikowany stan. Najprościej tworzyć nowe obiekty dla każdego przypadku albo jawnie kopiować dane.

Markery i polecenia pomagają panować nad większym zestawem testów

Nie każdy test powinien uruchamiać się przy każdej zmianie. Testy korzystające z bazy danych, sieci lub zewnętrznych usług bywają wolniejsze i mniej stabilne. Markery pozwalają oznaczyć grupy testów i uruchamiać je według potrzeb.

import pytest

@pytest.mark.integration
def test_user_repository():
    assert True

@pytest.mark.slow
def test_large_import():
    assert True

W konfiguracji projektu warto zarejestrować własne markery, aby uniknąć literówek:

[tool.pytest.ini_options]
markers = [
    "integration: testy współpracujące z innymi usługami",
    "slow: testy wymagające więcej czasu",
]
addopts = "--strict-markers"

Przykładowe uruchomienia wyglądają tak:

pytest -m "not integration"
pytest -m integration
pytest -k "user and not slow"
pytest tests/test_calculator.py::test_add_returns_sum

Opcja -m wybiera marker, -k filtruje testy po nazwie, a ostatnie polecenie uruchamia konkretną funkcję. Selektywne uruchamianie jest szczególnie pomocne przy diagnozowaniu awarii, bo nie trzeba czekać na cały zestaw testów.

Markery nie powinny jednak służyć do ukrywania niestabilnych testów. Jeżeli test raz przechodzi, a raz nie, trzeba znaleźć przyczynę, na przykład zależność od czasu, kolejności uruchamiania, sieci albo wspólnego stanu. Oznaczenie go jako wolnego nie naprawia problemu.

Mockowanie, testy integracyjne i granice narzędzia

Test jednostkowy powinien być szybki i przewidywalny, dlatego często izoluje się w nim wywołania zewnętrzne. Fixture monkeypatch pozwala tymczasowo zmienić funkcję, zmienną środowiskową lub element słownika, a po zakończeniu testu przywraca poprzedni stan.

import os

def current_environment():
    return os.getenv("APP_ENV", "production")

def test_current_environment(monkeypatch):
    monkeypatch.setenv("APP_ENV", "testing")
    assert current_environment() == "testing"

To dobre rozwiązanie, gdy sprawdzam reakcję kodu na konfigurację albo chcę odciąć test od systemu. Nie należy jednak mockować wszystkiego. Jeżeli każdy element jest zastąpiony atrapą, test może przechodzić mimo błędnej współpracy prawdziwych komponentów.

W przypadku klienta API warto połączyć dwa poziomy kontroli. Test jednostkowy może sprawdzić obsługę odpowiedzi za pomocą mocka, a osobny test integracyjny powinien potwierdzić, że aplikacja poprawnie komunikuje się z rzeczywistym serwisem lub kontrolowanym środowiskiem testowym. Mock chroni szybkość, ale test integracyjny chroni przed błędnymi założeniami o zewnętrznym systemie.

Pytest dobrze współpracuje też z istniejącymi testami opartymi na unittest. Migracja nie musi oznaczać przepisywania całego projektu. Można uruchamiać starsze klasy razem z nowymi funkcjami, a później stopniowo przenosić najbardziej kłopotliwe fragmenty.

Ograniczeniem pytest nie jest brak możliwości, lecz ryzyko nadmiernej rozbudowy konfiguracji. Duża liczba pluginów, automatycznych fixture i ukrytych zależności może utrudnić zrozumienie testu. Gdy do jego przeczytania trzeba znać kilka plików konfiguracyjnych, warto uprościć strukturę.

Jak zbudować zestaw testów, który pomaga zamiast przeszkadzać

Najczęstszy błąd początkujących polega na testowaniu szczegółów implementacji zamiast zachowania. Jeśli zmiana nazwy prywatnej metody łamie kilkanaście testów, mimo że publiczny efekt działania się nie zmienił, testy są zbyt mocno związane z kodem wewnętrznym. Lepiej sprawdzać wejście, wynik i istotne skutki uboczne.

Drugi problem to zbyt duże testy. Funkcja testowa powinna odpowiadać na jedno konkretne pytanie. Gdy sprawdza jednocześnie zapis do bazy, wysłanie wiadomości, format odpowiedzi i logowanie błędów, awaria nie daje jasnej informacji, co właściwie przestało działać.

  • Izoluj stan, aby jeden test nie wpływał na kolejny.
  • Opisuj przypadki nazwami wskazującymi oczekiwane zachowanie.
  • Trzymaj dane blisko testu, chyba że są współdzielone z dobrego powodu.
  • Uruchamiaj testy lokalnie i w CI w możliwie podobnym środowisku.
  • Naprawiaj testy niestabilne, zamiast regularnie je pomijać.

W procesie CI zwykle zaczynam od polecenia pytest, a dopiero później dodaję raportowanie, pomiar pokrycia i testy oznaczone jako integracyjne. Sam procent pokrycia nie mówi, czy testy są dobre. Można osiągnąć wysoką wartość, sprawdzając kod powierzchownie, bez uwzględnienia błędnych danych i granicznych warunków.

Praktyczny układ projektu może wyglądać następująco:

project/
├── src/
│   └── calculator.py
├── tests/
│   ├── test_calculator.py
│   └── conftest.py
├── pyproject.toml
└── README.md

W małych projektach katalog testów może znajdować się obok kodu aplikacji. W większych rozwiązaniach oddzielenie src i tests pomaga wykryć błędy importów oraz przypadkowe korzystanie z plików dostępnych tylko w katalogu roboczym.

Od pierwszego testu do stabilnej automatyzacji

Najrozsądniejszy start to mały zestaw testów dla funkcji, które mają wyraźne reguły i są często zmieniane. Najpierw sprawdzam poprawne dane, potem przypadki brzegowe, wyjątki i zależności zewnętrzne. Taka kolejność szybko pokazuje, czy testy rzeczywiście chronią najważniejsze zachowania aplikacji.

Nie trzeba od razu budować rozbudowanego systemu fixture ani pisać testów dla każdej linii. Lepsze są testy czytelne i stabilne niż duży zbiór przypadków, których nikt nie chce uruchamiać ani naprawiać. Pytest daje wygodne narzędzia, ale jakość automatyzacji nadal zależy od decyzji projektowych.

Jeśli zachowam izolację, ograniczę ukryte zależności i będę uruchamiać szybkie testy przy każdej zmianie, biblioteka zacznie działać jak praktyczna siatka bezpieczeństwa. Właśnie wtedy testowanie przestaje być obowiązkiem wykonywanym na końcu projektu, a staje się naturalnym elementem tworzenia lepszego kodu.

FAQ - Najczęstsze pytania

Utwórz środowisko wirtualne, zainstaluj pytest poleceniem python -m pip install pytest, a następnie zapisz test w pliku test_*.py. Test uruchomisz przez pytest lub python -m pytest.

Fixture przygotowują dane i zasoby potrzebne testom, na przykład użytkownika, plik lub połączenie. Pytest przekazuje fixture do testu jako argument, a wspólne fixture można umieścić w pliku conftest.py. Instrukcja yield pozwala wykonać dodatkowe sprzątanie po teście.

Parametryzację warto stosować, gdy tę samą funkcję trzeba sprawdzić dla wielu wariantów danych, takich jak wartości dodatnie, ujemne, zero lub puste dane. Dekorator pytest.mark.parametrize uruchamia test osobno dla każdego zestawu i wskazuje konkretny przypadek, który się nie powiódł.

Oznacz testy własnymi markerami, na przykład integration i slow, a następnie zarejestruj je w konfiguracji projektu z opcją --strict-markers. Polecenie pytest -m "not integration" pomija testy integracyjne, a pytest -m integration uruchamia tylko tę grupę.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

pytest fixture parametryzacja mockowanie markery

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