Mockowanie i fixture'y w testach Python – izolacja i zarządzanie zależnościami

Kacper Sieradziński
Kacper Sieradziński
1 lipca 2025Edukacja4 min czytania

Pisanie testów, które są szybkie, niezawodne i łatwe w utrzymaniu, wymaga umiejętności izolacji testów od zewnętrznych zależności. Czy testujesz funkcję, która wysyła emaile, pobiera dane z API, czy zapisuje pliki? Te operacje są wolne, mogą wymagać konfiguracji lub mogą nie działać w środowisku testowym. Mockowaniefixture'y to narzędzia, które pozwalają testować kod w izolacji, symulując zależności zewnętrzne. W tym artykule poznasz, jak używać unittest.mock, fixture'ów pytest i innych technik do tworzenia szybkich, niezawodnych testów.

Obraz główny Mockowanie i fixture'y w testach Python – izolacja i zarządzanie zależnościami

Dlaczego mockować?

Mockowanie (ang. mocking) to technika polegająca na zastępowaniu prawdziwych obiektów i funkcji ich atrapami podczas testów. Korzyści:

  • Szybkość – mocki działają błyskawicznie, nie wykonują prawdziwych operacji
  • 🔒 Izolacja – testujesz tylko swój kod, bez zależności od zewnętrznych serwisów
  • 🎯 Przewidywalność – kontrolujesz dokładnie, co zwraca mock
  • 🛡️ Niezawodność – testy nie zależą od dostępności API, bazy danych czy sieci
  • 💰 Oszczędność – nie musisz płacić za użycie zewnętrznych serwisów

Jeśli dopiero zaczynasz z testowaniem, sprawdź najpierw wprowadzenie do unittest lub testowanie z pytest.

Podstawy unittest.mock

Moduł unittest.mock (dostępny również jako mock w starszych wersjach Pythona) to standardowe narzędzie do mockowania w Pythonie.

Podstawowy Mock

Najprostszy mock to obiekt, który można skonfigurować, aby zwracał określone wartości:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from unittest.mock import Mock # Tworzenie mocka mock_api = Mock() # Konfiguracja zachowania mock_api.get.return_value = {"status": "ok", "data": [1, 2, 3]} # Użycie result = mock_api.get() print(result) # {'status': 'ok', 'data': [1, 2, 3]} # Weryfikacja, że metoda została wywołana mock_api.get.assert_called_once()

Mock zwracający różne wartości

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 mock_api = Mock() mock_api.fetch_data.side_effect = [ {"data": "first"}, {"data": "second"}, Exception("API Error") ] print(mock_api.fetch_data()) # {'data': 'first'} print(mock_api.fetch_data()) # {'data': 'second'} try: mock_api.fetch_data() # Rzuca Exception("API Error") except Exception as e: print(e)

Przykład praktyczny – mockowanie API

Załóżmy, że mamy funkcję pobierającą dane z API:

Python
1 2 3 4 5 6 7 # api_client.py import requests def get_user_data(user_id): response = requests.get(f"https://api.example.com/users/{user_id}") response.raise_for_status() return response.json()

Test z mockowaniem:

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 # test_api_client.py from unittest.mock import Mock, patch import pytest from api_client import get_user_data def test_get_user_data_success(): # Mock odpowiedzi HTTP mock_response = Mock() mock_response.json.return_value = {"id": 1, "name": "Jan", "email": "jan@example.com"} mock_response.raise_for_status = Mock() # Mock funkcji requests.get with patch('api_client.requests.get', return_value=mock_response): result = get_user_data(1) assert result["name"] == "Jan" assert result["email"] == "jan@example.com" def test_get_user_data_error(): # Mock błędu HTTP mock_response = Mock() mock_response.raise_for_status.side_effect = requests.HTTPError("Not Found") with patch('api_client.requests.get', return_value=mock_response): with pytest.raises(requests.HTTPError): get_user_data(999)

Patch – tymczasowe zastępowanie obiektów

patch to dekorator/context manager, który tymczasowo zastępuje obiekt w określonym namespace.

patch jako dekorator

Python
1 2 3 4 5 6 7 8 9 10 11 from unittest.mock import patch @patch('api_client.requests.get') def test_get_user_data(mock_get): mock_response = Mock() mock_response.json.return_value = {"id": 1, "name": "Jan"} mock_response.raise_for_status = Mock() mock_get.return_value = mock_response result = get_user_data(1) assert result["name"] == "Jan"

patch jako context manager

Python
1 2 3 4 5 6 7 8 9 def test_get_user_data(): with patch('api_client.requests.get') as mock_get: mock_response = Mock() mock_response.json.return_value = {"id": 1, "name": "Jan"} mock_response.raise_for_status = Mock() mock_get.return_value = mock_response result = get_user_data(1) assert result["name"] == "Jan"

patch.object – patchowanie atrybutu obiektu

Python
1 2 3 4 5 6 7 8 9 10 11 from unittest.mock import patch, MagicMock class Database: def connect(self): return "real connection" def test_with_patch_object(): db = Database() with patch.object(db, 'connect', return_value="mock connection"): result = db.connect() assert result == "mock connection"

patch.multiple – patchowanie wielu obiektów

Python
1 2 3 4 5 6 from unittest.mock import patch @patch.multiple('module', func1=Mock(), func2=Mock()) def test_multiple_patches(): # Oba funkcje są zmockowane pass

MagicMock vs Mock

MagicMock to rozszerzenie Mock, które automatycznie tworzy mocki dla wszystkich metod i atrybutów:

Python
1 2 3 4 5 6 7 8 9 10 from unittest.mock import Mock, MagicMock # Mock - trzeba ręcznie tworzyć metody mock1 = Mock() mock1.some_method() # AttributeError, jeśli nie skonfigurowane # MagicMock - automatycznie tworzy metody mock2 = MagicMock() mock2.some_method() # Działa, zwraca kolejny MagicMock mock2.any_attribute # Działa, zwraca kolejny MagicMock

Użycie MagicMock:

Python
1 2 3 4 5 6 7 8 9 10 11 from unittest.mock import MagicMock def test_magic_mock(): mock_obj = MagicMock() mock_obj.process_data.return_value = {"processed": True} result = mock_obj.process_data("input") assert result == {"processed": True} # MagicMock zapamiętuje wszystkie wywołania mock_obj.process_data.assert_called_once_with("input")

Weryfikacja wywołań mocków

Mocki pozwalają weryfikować, jak zostały wywołane:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from unittest.mock import Mock, call mock_func = Mock() mock_func(1, 2, 3) mock_func(4, 5, 6, keyword="value") # Sprawdzanie liczby wywołań assert mock_func.call_count == 2 # Sprawdzanie argumentów ostatniego wywołania mock_func.assert_called_with(4, 5, 6, keyword="value") # Sprawdzanie wszystkich wywołań expected_calls = [call(1, 2, 3), call(4, 5, 6, keyword="value")] mock_func.assert_has_calls(expected_calls) # Sprawdzanie, czy nie został wywołany mock_func.reset_mock() mock_func.assert_not_called()

Mockowanie w pytest – monkeypatch

pytest oferuje wbudowaną fixture monkeypatch, która pozwala tymczasowo modyfikować obiekty:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import pytest import os def test_environment_variable(monkeypatch): # Ustawienie zmiennej środowiskowej monkeypatch.setenv("API_KEY", "test_key") assert os.getenv("API_KEY") == "test_key" def test_module_attribute(monkeypatch): import api_client # Zastąpienie atrybutu modułu monkeypatch.setattr(api_client, "API_URL", "http://test.example.com") assert api_client.API_URL == "http://test.example.com" def test_function(monkeypatch): def mock_function(): return "mocked" import module monkeypatch.setattr(module, "original_function", mock_function) assert module.original_function() == "mocked"

monkeypatch vs patch

  • monkeypatch – pytest-specific, działa tylko w kontekście testów pytest
  • patch – uniwersalne, działa wszędzie, bardziej elastyczne

Fixture'y pytest – zaawansowane użycie

Fixture'y są szczególnie przydatne do zarządzania mockami i zasobami testowymi. Więcej podstaw o fixture'ach znajdziesz w artykule o pytest.

Fixture z mockiem

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 import pytest from unittest.mock import Mock, patch @pytest.fixture def mock_api(): mock = Mock() mock.get.return_value = {"status": "ok"} return mock def test_with_mock_fixture(mock_api): with patch('module.requests', mock_api): # Twój kod używający API pass

Fixture z cleanup

Python
1 2 3 4 5 6 7 8 @pytest.fixture def temporary_file(tmp_path): """Tworzy tymczasowy plik i usuwa go po teście""" file_path = tmp_path / "test.txt" file_path.write_text("test content") yield file_path # Cleanup automatycznie wykona się po teście # W tym przypadku pytest automatycznie usuwa tmp_path

Fixture z kontekstem

Python
1 2 3 4 5 6 7 8 9 10 @pytest.fixture def database_connection(): """Fixture zarządzający połączeniem z bazą danych""" conn = create_test_connection() yield conn conn.close() def test_with_database(database_connection): result = database_connection.query("SELECT 1") assert result is not None

conftest.py dla wspólnych fixture'ów

Plik conftest.py pozwala udostępnić fixture'y wszystkim testom:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # tests/conftest.py import pytest from unittest.mock import Mock @pytest.fixture def mock_external_api(): mock = Mock() mock.fetch_data.return_value = {"data": "test"} return mock @pytest.fixture def sample_user(): return { "id": 1, "name": "Test User", "email": "test@example.com" }

Teraz wszystkie testy w katalogu tests/ mają dostęp do tych fixture'ów:

Python
1 2 3 4 # tests/test_user.py def test_user_processing(mock_external_api, sample_user): # Oba fixture'y są automatycznie dostępne pass

Zaawansowane techniki mockowania

Mockowanie datetime

Często potrzebujesz kontrolować czas w testach:

Python
1 2 3 4 5 6 7 8 9 10 11 12 from unittest.mock import patch from datetime import datetime def get_current_time(): return datetime.now() @patch('module.datetime') def test_current_time(mock_datetime): mock_datetime.now.return_value = datetime(2024, 1, 1, 12, 0, 0) result = get_current_time() assert result.year == 2024 assert result.month == 1

Mockowanie random

Python
1 2 3 4 5 6 7 8 import random from unittest.mock import patch @patch('module.random.randint') def test_random_number(mock_randint): mock_randint.return_value = 42 result = random.randint(1, 100) assert result == 42

Mockowanie metod klasy

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 from unittest.mock import patch class UserService: def get_user(self, user_id): # Prawdziwa implementacja pass @patch.object(UserService, 'get_user') def test_user_service(mock_get_user): mock_get_user.return_value = {"id": 1, "name": "Jan"} service = UserService() result = service.get_user(1) assert result["name"] == "Jan"

Mockowanie z side_effect dla różnych scenariuszy

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from unittest.mock import Mock def test_multiple_scenarios(): mock_api = Mock() mock_api.fetch.side_effect = [ {"data": "success"}, # Pierwsze wywołanie Exception("API Error"), # Drugie wywołanie {"data": "retry success"} # Trzecie wywołanie ] # Symulacja różnych scenariuszy w jednym teście assert mock_api.fetch() == {"data": "success"} try: mock_api.fetch() except Exception as e: assert str(e) == "API Error" assert mock_api.fetch() == {"data": "retry success"}

Mockowanie w testach integracyjnych

W testach integracyjnych często mockujesz tylko część zależności:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import pytest from unittest.mock import patch, Mock from fastapi.testclient import TestClient @pytest.fixture def mock_email_service(): with patch('app.services.email.send') as mock: yield mock def test_user_registration(mock_email_service): client = TestClient(app) # Wysłanie prawdziwego żądania HTTP response = client.post("/register", json={ "email": "user@example.com", "password": "password123" }) assert response.status_code == 201 # Weryfikacja, że email został "wysłany" (zmockowany) mock_email_service.assert_called_once()

Wzorce i najlepsze praktyki

1. Mockuj na właściwym poziomie

Mockuj obiekty, które są zależnościami Twojego kodu, nie sam kod, który testujesz:

Python
1 2 3 4 5 6 7 8 9 # ✅ Dobrze - mockujemy zewnętrzną zależność @patch('module.requests.get') def test_api_call(mock_get): pass # ❌ Źle - mockujemy kod, który testujemy @patch('module.process_data') def test_process_data(mock_process): pass # To nie ma sensu!

2. Używaj fixture'ów dla wspólnych mocków

Jeśli ten sam mock jest używany w wielu testach, stwórz fixture:

Python
1 2 3 4 5 @pytest.fixture def mock_database(): with patch('module.Database.connect') as mock: mock.return_value = MockConnection() yield mock

3. Sprawdzaj, czy mocki zostały wywołane

Mocki nie tylko zwracają wartości, ale też weryfikują wywołania:

Python
1 2 3 4 5 6 def test_api_call(mock_api): result = fetch_data() # Weryfikuj nie tylko wynik, ale też wywołania mock_api.get.assert_called_once_with("/data") assert result is not None

4. Unikaj over-mockowania

Nie mockuj wszystkiego. Czasami lepiej użyć prawdziwych obiektów (np. w testach integracyjnych):

Python
1 2 3 4 5 6 7 8 9 # ✅ Test integracyjny z prawdziwą bazą testową def test_user_creation(db_session): user = User.create(name="Jan") assert user.id is not None # ❌ Niepotrzebne mockowanie w teście integracyjnym def test_user_creation(mock_db): # To nie jest prawdziwy test integracyjny pass

5. Używaj spec dla lepszej walidacji

Parametr spec w Mock sprawdza, czy używasz tylko istniejących atrybutów:

Python
1 2 3 4 5 6 from unittest.mock import Mock # Mock z spec - waliduje atrybuty mock_api = Mock(spec=['get', 'post']) mock_api.get() # OK mock_api.delete() # AttributeError - delete nie jest w spec

6. Automatyczny cleanup z yield

Zawsze używaj yield w fixture'ach, które wymagają cleanup:

Python
1 2 3 4 5 6 @pytest.fixture def temp_directory(tmp_path): test_dir = tmp_path / "test" test_dir.mkdir() yield test_dir # Cleanup (w tym przypadku automatyczny)

Typowe pułapki i jak ich unikać

Pułapka 1: Mockowanie w złym namespace

Python
1 2 3 4 5 6 7 8 9 10 11 # ❌ Źle - patchujemy w złym miejscu from module import get_user_data @patch('requests.get') # Błąd! get_user_data używa module.requests def test_user(mock_get): pass # ✅ Dobrze - patchujemy tam, gdzie jest używane @patch('module.requests.get') def test_user(mock_get): pass

Pułapka 2: Zapominanie o autospec

Python
1 2 3 4 5 6 7 # ❌ Mock może mieć dowolne atrybuty (typo nie zostanie wykryte) mock = Mock() mock.gett() # Działa (typo), ale nie powinno # ✅ autospec sprawdza typ mock = Mock(autospec=RealObject) mock.gett() # AttributeError - typo zostanie wykryte

Pułapka 3: Mockowanie zamiast fixture'ów

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # ❌ Mock w każdym teście def test1(): with patch('module.api') as mock: pass def test2(): with patch('module.api') as mock: # Duplikacja pass # ✅ Wspólny fixture @pytest.fixture def mock_api(): with patch('module.api') as mock: yield mock def test1(mock_api): pass def test2(mock_api): pass

Podsumowanie

Mockowanie i fixture'y to potężne narzędzia do tworzenia szybkich, niezawodnych testów. Kluczowe punkty:

  • ✅ Używaj unittest.mock do mockowania zależności zewnętrznych
  • patch tymczasowo zastępuje obiekty w namespace
  • monkeypatch w pytest to alternatywa dla patch
  • ✅ Fixture'y zarządzają zasobami testowymi i mockami
  • ✅ Weryfikuj wywołania mocków, nie tylko wyniki
  • ✅ Unikaj over-mockowania w testach integracyjnych

Co dalej?

Teraz, gdy znasz mockowanie i fixture'y, możesz zastosować je w praktyce:

Pamiętaj: mockowanie to narzędzie do izolacji testów, ale nie zastępuje testów integracyjnych. Używaj mocków tam, gdzie są potrzebne, ale testuj również współpracę komponentów!