Streszczenie
  • Dlaczego warto testować?
  • Rodzaje testów
  • Testy jednostkowe (Unit tests)
  • Podstawowy przykład z pytest
Testowanie aplikacji i API w Pythonie

Testowanie to nie dodatek do projektu — to jego fundament. Każda profesjonalna aplikacja powinna być pokryta testami, które weryfikują jej poprawność, chronią przed regresjami i dokumentują zachowanie systemu. W Pythonie testowanie jest ułatwione dzięki frameworkom takim jak pytest, unittest i Django TestCase, które oferują zwięzłą składnię, automatyczne wykrywanie testów i integrację z CI/CD. W tym przewodniku poznasz, jak pisać różne typy testów dla aplikacji webowych — od prostych testów jednostkowych po kompleksowe testy integracyjne API.

Kurs · 24 lekcje8h 14m
Kurs

Kurs Python od podstaw — PyStart

Zacznij programować w Pythonie od zera. Praktyczny kurs wideo z ćwiczeniami — bez wcześniejszego doświadczenia.

  • 24 lekcje wideo + 80 ćwiczeń
  • Realne bazy z e-commerce
  • Społeczność i code-review
699 zł
Zacznij kurs Pythona

Dlaczego warto testować?

Pisanie testów to inwestycja, która:

  • zwiększa stabilność aplikacji — testy wykrywają błędy przed trafieniem do produkcji,
  • pozwala na bezpieczną refaktoryzację — masz pewność, że zmiany nie psują istniejącej funkcjonalności,
  • dokumentuje kod — testy pokazują, jak kod powinien być używany,
  • zmniejsza stres przy wdrożeniach — wiedzisz, że aplikacja działa poprawnie przed deploymentem.

Brak testów to nie oszczędność czasu — to przesunięcie kosztów na przyszłość, gdy błędy wyjdą w produkcji i będą wymagały natychmiastowego fixa, często pod presją czasu i obawą o wpływ na użytkowników.

Rodzaje testów

W projektach Pythonowych najczęściej spotkasz trzy typy testów:

Typ testuCelZakres
Unit testWeryfikuje pojedynczą funkcję lub metodęMały (moduł)
Integration testSprawdza współpracę wielu komponentówŚredni
End-to-End / API testSprawdza całe zachowanie aplikacjiDuży (pełna aplikacja)

Dobrze zorganizowany projekt ma testy na wszystkich trzech poziomach. Testy jednostkowe są szybkie i izolowane, testy integracyjne weryfikują współpracę między komponentami, a testy E2E sprawdzają cały flow użytkownika.

Testy jednostkowe (Unit tests)

Testy jednostkowe to najprostszy i najczęstszy rodzaj testów. Sprawdzają, czy konkretna funkcja działa zgodnie z założeniami. Powinny być szybkie, izolowane i testować tylko jedną rzecz naraz.

Podstawowy przykład z pytest

Python
1 2 3 4 5 6 7 def suma(a, b): return a + b def test_suma_dodaje_dwie_liczby(): assert suma(2, 3) == 5 assert suma(0, 0) == 0 assert suma(-1, 1) == 0

Aby uruchomić testy:

Bash
1 pytest

pytest automatycznie znajdzie i wykona wszystkie testy w plikach zaczynających się od test_ lub kończących na _test.py. Możesz też uruchomić konkretny plik lub test:

Bash
1 2 pytest test_math.py pytest test_math.py::test_suma_dodaje_dwie_liczby

Parametryzacja testów

pytest pozwala na parametryzację testów, co eliminuje duplikację kodu:

Python
1 2 3 4 5 6 7 8 9 10 import pytest @pytest.mark.parametrize("a, b, expected", [ (2, 3, 5), (0, 0, 0), (-1, 1, 0), (-5, -3, -8), ]) def test_suma_parametrized(a, b, expected): assert suma(a, b) == expected

Fixtures – powtarzalne środowisko testowe

Fixtures pozwalają przygotować dane lub konfigurację przed uruchomieniem testu. To eliminuje duplikację kodu i zapewnia spójne środowisko testowe.

Podstawowy fixture

Python
1 2 3 4 5 6 7 8 9 import pytest @pytest.fixture def sample_data(): return {"username": "admin", "password": "1234"} def test_user_data(sample_data): assert "username" in sample_data assert sample_data["username"] == "admin"

Zaawansowane fixtures

Fixtures mogą inicjalizować bazy danych, logować użytkowników, przygotowywać API lub mockować zewnętrzne serwisy:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @pytest.fixture def client(): """Klient testowy Flask""" from app import app app.config['TESTING'] = True with app.test_client() as client: yield client @pytest.fixture def db_session(): """Tymczasowa sesja bazy danych""" from app import db db.create_all() yield db.session db.drop_all() def test_create_user(client, db_session): response = client.post('/api/users', json={ 'username': 'testuser', 'email': 'test@example.com' }) assert response.status_code == 201

Fixtures mogą również mieć zakres (scope) — function (domyślny), class, module, session — co pozwala na optymalizację wydajności testów.

Testy integracyjne

Testy integracyjne sprawdzają, czy różne części aplikacji działają razem poprawnie. Przykład: czy widok w Django poprawnie komunikuje się z modelem i zwraca odpowiedź HTTP.

Przykład w Django

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from django.test import TestCase from django.urls import reverse from myapp.models import Post class PostViewTests(TestCase): def setUp(self): """Przygotowanie danych przed każdym testem""" self.post = Post.objects.create( title="Test Post", content="Test content" ) def test_post_list_returns_200(self): response = self.client.get(reverse("post_list")) self.assertEqual(response.status_code, 200) self.assertContains(response, "Test Post") def test_post_detail_view(self): response = self.client.get(reverse("post_detail", args=[self.post.id])) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['post'], self.post) def test_create_post(self): response = self.client.post(reverse("post_create"), { 'title': 'New Post', 'content': 'New content' }) self.assertEqual(response.status_code, 302) # Redirect after creation self.assertTrue(Post.objects.filter(title='New Post').exists())

W tym teście Django automatycznie tworzy tymczasową bazę danych, dzięki czemu nie musisz modyfikować danych produkcyjnych. setUp() jest uruchamiane przed każdym testem w klasie, co pozwala na przygotowanie wspólnych danych.

Testy API w FastAPI

Newsletter · co środę

Python co tydzień — newsletter dla programistów

Otrzymuj codzienne ćwiczenia, ciekawostki z ekosystemu Pythona i wskazówki do rozmów rekrutacyjnych.

2 312 czytelników · ⭐ 4,8

FastAPI ma wbudowany TestClient, który ułatwia testowanie endpointów API. TestClient działa bez uruchamiania serwera — wywołuje endpointy bezpośrednio w pamięci, co sprawia, że testy są bardzo szybkie.

Podstawowy test API

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from fastapi.testclient import TestClient from main import app client = TestClient(app) def test_get_root(): response = client.get("/") assert response.status_code == 200 assert response.json() == {"message": "Hello World"} def test_create_item(): response = client.post("/items/", json={ "name": "Test Item", "price": 10.99 }) assert response.status_code == 201 data = response.json() assert data["name"] == "Test Item" assert "id" in data

Testy z autoryzacją

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 def test_protected_endpoint(): # Najpierw zaloguj się login_response = client.post("/login", json={ "username": "testuser", "password": "testpass" }) token = login_response.json()["access_token"] # Użyj tokena w nagłówku headers = {"Authorization": f"Bearer {token}"} response = client.get("/protected", headers=headers) assert response.status_code == 200 # Alternatywnie z fixture @pytest.fixture def authenticated_client(): client = TestClient(app) login_response = client.post("/login", json={ "username": "testuser", "password": "testpass" }) token = login_response.json()["access_token"] client.headers = {"Authorization": f"Bearer {token}"} return client def test_protected_with_fixture(authenticated_client): response = authenticated_client.get("/protected") assert response.status_code == 200

Testy API w FastAPI są niezwykle szybkie, bo działają bez serwera — wywołują endpointy bezpośrednio w pamięci. To sprawia, że możesz uruchomić setki testów w sekundach.

TDD – Test Driven Development

TDD to podejście, w którym najpierw piszesz testy, a dopiero potem implementację. Proces wygląda tak:

  1. Red — Napisz test, który na początku nie przejdzie (bo funkcja jeszcze nie istnieje),
  2. Green — Zaimplementuj minimalny kod, aby test przeszedł,
  3. Refactor — Refaktoryzuj, zachowując poprawność testów.

Przykład TDD

Najpierw test:

Python
1 2 3 4 # test_math.py def test_dodawanie(): assert dodaj(2, 2) == 4 assert dodaj(-1, 1) == 0

Test nie przejdzie, bo funkcja jeszcze nie istnieje. Teraz implementacja:

Python
1 2 3 # math_utils.py def dodaj(a, b): return a + b

Teraz test przejdzie. Jeśli chcesz dodać więcej funkcjonalności (np. obsługę więcej niż dwóch argumentów), najpierw napisz test, a potem zaktualizuj implementację.

To prosty przykład, ale zasada jest kluczowa: najpierw weryfikacja, potem implementacja. TDD zmusza Cię do myślenia o interfejsie funkcji i przypadkach brzegowych przed napisaniem kodu, co prowadzi do lepszego designu.

Mockowanie zależności

W testach często chcesz „odłączyć" zewnętrzne API, bazę danych lub system plików. Do tego służy mocking. Mockowanie pozwala testować logikę bez rzeczywistego wykonywania zewnętrznych operacji, co czyni testy szybkimi i niezależnymi od zewnętrznych serwisów.

Przykład z unittest.mock

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from unittest.mock import patch, MagicMock import requests @patch("requests.get") def test_fetch_data(mock_get): # Konfiguracja mocka mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"ok": True, "data": "test"} mock_get.return_value = mock_response # Test logiki response = requests.get("https://api.example.com") assert response.status_code == 200 assert response.json()["ok"] is True # Weryfikacja wywołania mock_get.assert_called_once_with("https://api.example.com")

Mockowanie w pytest

pytest oferuje własną funkcjonalność do mockowania:

Python
1 2 3 4 5 6 7 8 9 10 import pytest from unittest.mock import Mock def test_with_mock(): mock_service = Mock() mock_service.process.return_value = "processed" result = my_function(mock_service) assert result == "processed" mock_service.process.assert_called_once()

Mockowanie pozwala testować logikę bez rzeczywistego wykonywania zewnętrznych operacji, co czyni testy szybkimi i niezależnymi od zewnętrznych serwisów.

Raporty z testów

pytest umożliwia generowanie szczegółowych raportów, co jest kluczowe dla integracji z CI/CD i analizy wyników testów.

Podstawowe opcje

Bash
1 2 3 4 5 6 7 8 9 10 11 # Tylko pierwszy błąd, bez ostrzeżeń, tryb cichy pytest --maxfail=1 --disable-warnings -q # Szczegółowy output pytest -v # Pokaż print statements pytest -s # Pokrycie kodu (wymaga pytest-cov) pytest --cov=myapp --cov-report=html

Raporty w formacie XML

Bash
1 pytest --junitxml=report.xml

Dzięki temu łatwo zintegrujesz testy z narzędziami CI/CD (GitHub Actions, GitLab CI), które mogą wyświetlać wyniki testów i pokrycie kodu.

Pokrycie kodu

Pokrycie kodu (code coverage) pokazuje, jaki procent kodu jest testowany:

Bash
1 2 3 pip install pytest-cov pytest --cov=myapp --cov-report=term-missing

Dążyć należy do wysokiego pokrycia, ale nie kosztem jakości testów — lepiej mieć mniej, ale lepszych testów, niż wiele słabych.

Integracja z CI/CD

Kurs · 24 lekcje8h 14m
Kurs

Kurs Python dla początkujących — PyStart

Zacznij programować w Pythonie! Idealne dla osób bez doświadczenia. Praktyczne zadania, projekty i wsparcie społeczności.

  • 24 lekcje wideo + 80 ćwiczeń
  • Realne bazy z e-commerce
  • Społeczność i code-review
699 zł
Rozpocznij naukę

Testy powinny być automatycznie uruchamiane przy każdym pushu lub pull requeście. To zapewnia, że każda zmiana jest zweryfikowana przed mergem.

Przykład workflow dla GitHub Actions

YAML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 name: Python Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.12 - name: Install dependencies run: | pip install -r requirements.txt pip install pytest pytest-cov - name: Run tests run: | pytest --cov=myapp --cov-report=xml - name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./coverage.xml

Dzięki temu każdy commit przechodzi przez testy automatycznie — zanim trafi do produkcji. CI/CD może również blokować mergowanie pull requestów, jeśli testy nie przechodzą.

Dobre praktyki testowania

Przy pisaniu testów warto pamiętać o kilku kluczowych zasadach:

  1. Każdy nowy feature = nowy test — jeśli dodajesz funkcjonalność, dodaj też test, który ją weryfikuje.

  2. Testuj tylko zachowanie, nie implementację — testy powinny sprawdzać, czy kod robi to, co powinien, a nie jak to robi. To pozwala na refaktoryzację bez łamania testów.

  3. Stosuj opisowe nazwy testów — nazwa testu powinna jasno komunikować, co testuje:

Python
1 2 3 4 5 6 7 # Dobrze def test_user_cannot_login_with_wrong_password(): pass # Źle def test_login(): pass
  1. Oddziel testy jednostkowe od integracyjnych — różne typy testów mogą mieć różne wymagania (baza danych, sieć) i różną szybkość wykonania.

  2. Używaj fixtures do powtarzalnych danych — unikaj duplikacji kodu i zapewnij spójność danych testowych.

  3. Mierz pokrycie kodu testami — użyj pytest-cov, aby zobaczyć, które części kodu nie są testowane. Dążyć należy do wysokiego pokrycia, ale nie kosztem jakości.

  4. Nie testuj bibliotek zewnętrznych — testuj swoją logikę, nie kod z innych bibliotek (np. Django, SQLAlchemy). Te biblioteki mają własne testy.

  5. Testuj przypadki brzegowe — nie tylko „happy path", ale także błędy, puste wartości, wartości graniczne.

  6. Testy powinny być izolowane — każdy test powinien móc być uruchomiony niezależnie, bez zależności od innych testów.

  7. Używaj Arrange-Act-Assert (AAA) — struktura testu powinna być czytelna:

Python
1 2 3 4 5 6 7 8 9 10 def test_example(): # Arrange - przygotowanie danych user = create_user() # Act - wykonanie akcji result = user.activate() # Assert - weryfikacja wyniku assert result is True assert user.is_active is True

Podsumowanie

Testowanie to nawyk, który oddziela amatorów od profesjonalistów. Dobrze zaprojektowany zestaw testów chroni przed regresjami, pozwala na bezpieczne wdrożenia, automatyzuje kontrolę jakości i zwiększa pewność siebie zespołu przy każdej zmianie.

Nieważne, czy tworzysz REST API w FastAPI, aplikację w Django czy mikroserwis w Flasku — testy to Twoja pierwsza linia obrony. Zacznij od prostych testów jednostkowych, stopniowo dodawaj testy integracyjne, a następnie rozbuduj o testy E2E w miarę potrzeb projektu. Pamiętaj, że testy to nie tylko narzędzie do weryfikacji — to także dokumentacja i zabezpieczenie na przyszłość.

Część 26 z 26

Python w Web Development

druga lekcja cyklu „Flask vs FastAPI: Porównanie frameworków webowych w Pythonie"

Czytaj kolejny →