Мокирование: зачем и когда
Вы написали 50 тестов для банковского приложения. Тесты работают 5 минут. Почему? Каждый тест обращается к реальной БД и внешнему API валют. Как ускорить?
Цель: Понять зачем нужно мокирование и когда его использовать.
Вы точно готовы?
Убедитесь, что прошли pytest-basics:
# Вы должны уметь писать тесты с фикстурами
@pytest.fixture
def account():
return BankAccount(100)
def test_deposit(account):
account.deposit(50)
assert account.balance == 150Если фикстуры или parametrize непонятны — вернитесь к Pytest с нуля.
Проблема: тесты с реальными зависимостями
Медленные тесты
# src/weather_service.py
import requests
class WeatherService:
def get_temperature(self, city):
"""Получает температуру из внешнего API"""
response = requests.get(
f"https://api.weather.com/current?city={city}"
)
data = response.json()
return data["temperature"]
# tests/test_weather.py
def test_get_temperature():
"""Тест получения температуры"""
service = WeatherService()
temperature = service.get_temperature("Moscow")
assert temperature > -50 # Реальный HTTP-запрос!Проблемы:
- ❌ Медленно: HTTP-запрос занимает 500ms
- ❌ Ненадёжно: Если API недоступен — тест падает
- ❌ Не изолировано: Тест зависит от интернета
- ❌ Не детерминировано: Погода меняется каждый день
Запускаем 50 тестов:
pytest tests/
# Время: 25 секунд (50 * 500ms)
# FAILED: ConnectionError (API недоступен)❌ Тесты не должны зависеть от внешних систем!
Непредсказуемые тесты
from datetime import datetime
def test_user_birthday():
"""Проверяем что пользователь получает скидку в день рождения"""
user = User("Alice", birthday="2000-01-15")
# ❌ Тест работает только 15 января!
if datetime.now().date() == user.birthday:
assert user.get_discount() == 0.2
else:
assert user.get_discount() == 0.0Проблема: Тест работает по-разному в зависимости от даты запуска!
Побочные эффекты
def test_save_report():
"""Тест сохранения отчёта"""
report = Report(data=[1, 2, 3])
# ❌ Записываем реальный файл на диск!
report.save_to_file("report.csv")
# Файл создан — нужно удалить вручную
assert os.path.exists("report.csv")Проблемы:
- ❌ Создаются реальные файлы
- ❌ Нужен cleanup
- ❌ Могут быть конфликты (файл уже существует)
Решение: мокирование (test doubles)
Что такое мокирование
Мокирование — замена реальных зависимостей на "поддельные" объекты для тестирования.
Пример:
# Было: реальный HTTP-запрос
response = requests.get("https://api.weather.com/...")
# Стало: мок возвращает фиксированные данные
mock_response = Mock()
mock_response.json.return_value = {"temperature": 15}✅ Тест работает мгновенно и предсказуемо!
Test doubles: типы поддельных объектов
# 1. STUB — возвращает фиксированные данные
class StubWeatherAPI:
def get_temperature(self, city):
return 15 # Всегда 15°C
# 2. MOCK — проверяет вызовы + возвращает данные
class MockWeatherAPI:
def __init__(self):
self.calls = []
def get_temperature(self, city):
self.calls.append(city) # Запоминаем вызов
return 15
# 3. FAKE — рабочая имплементация (но упрощённая)
class FakeDatabase:
def __init__(self):
self.data = {} # Не реальная БД, а dict
def save(self, key, value):
self.data[key] = value
def get(self, key):
return self.data.get(key)
# 4. SPY — обёртка над реальным объектом (записывает вызовы)
class SpyWeatherAPI:
def __init__(self, real_api):
self.real_api = real_api
self.calls = []
def get_temperature(self, city):
self.calls.append(city)
return self.real_api.get_temperature(city) # Реальный вызов!Когда использовать:
| Тип | Назначение | Пример |
|---|---|---|
| Stub | Простые данные без проверки вызовов | API возвращает фиксированный JSON |
| Mock | Проверка взаимодействия (сколько раз вызвали) | Убедиться что email отправлен 1 раз |
| Fake | Рабочая in-memory имплементация | In-memory БД для быстрых тестов |
| Spy | Проверка реального объекта (редко) | Логирование реальных вызовов |
В pytest обычно используют Mock и Stub.
Пример: без мока VS с моком
Без мока (медленно):
def test_weather_forecast():
"""Тест прогноза погоды (медленный)"""
service = WeatherService()
# Реальный HTTP-запрос (500ms)
forecast = service.get_forecast("Moscow", days=7)
assert len(forecast) == 7
# Тест может упасть если API недоступен!С моком (быстро):
from unittest.mock import Mock
def test_weather_forecast_mocked():
"""Тест прогноза погоды (быстрый)"""
# Создаём мок API
mock_api = Mock()
mock_api.get_forecast.return_value = [
{"day": 1, "temp": 15},
{"day": 2, "temp": 16},
# ... 7 дней
]
service = WeatherService(api=mock_api)
# Мгновенный вызов (< 1ms)
forecast = service.get_forecast("Moscow", days=7)
assert len(forecast) == 7
# Проверяем что API вызван правильно
mock_api.get_forecast.assert_called_once_with("Moscow", days=7)✅ Тест работает мгновенно, не зависит от сети!
Когда использовать мокирование
✅ Используйте моки для:
1. Внешние системы (HTTP, API)
# Мокируем requests
@patch("requests.get")
def test_fetch_user(mock_get):
mock_get.return_value.json.return_value = {"name": "Alice"}
# Тест без реального HTTP2. Файловый ввод-вывод
# Мокируем open()
@patch("builtins.open", mock_open(read_data="test data"))
def test_read_file():
# Тест без реальных файлов3. Время и даты
# Мокируем datetime.now()
@freeze_time("2025-01-15")
def test_birthday_discount():
# Тест всегда работает как будто сегодня 15 января4. База данных
# Используем fake in-memory БД
@pytest.fixture
def fake_db():
return FakeDatabase()
def test_save_user(fake_db):
fake_db.save("user_1", {"name": "Alice"})
# Быстрый тест без реальной БД5. Медленные операции
# Мокируем медленную функцию
@patch("image_processor.resize_image")
def test_upload_avatar(mock_resize):
mock_resize.return_value = "resized.jpg"
# Тест без реальной обработки изображений❌ НЕ мокируйте:
1. Чистую бизнес-логику
# ❌ ПЛОХО — мокируем метод который тестируем
def test_calculate_discount():
order = Order(total=100)
order.calculate_discount = Mock(return_value=90) # Бессмысленно!
result = order.calculate_discount()
assert result == 90 # Тестируем мок, не код!2. Код внутри тестируемого модуля
# ✅ ХОРОШО — тестируем реальную логику
def test_calculate_total():
cart = ShoppingCart()
cart.add_item(price=100, quantity=2)
# Реальное вычисление
total = cart.calculate_total()
assert total == 2003. Простые объекты (User, BankAccount)
# ❌ ПЛОХО
def test_user():
user = Mock(name="Alice", age=25) # Зачем?
# ✅ ХОРОШО
def test_user():
user = User("Alice", 25) # Реальный объект быстрыйПрактические рекомендации
Правило 1: Мокируйте границы системы
Ваш код → [Мок HTTP] → Внешний API
Ваш код → [Мок DB] → PostgreSQL
Ваш код → [Мок File] → Файловая система✅ Мокируйте всё что выходит за пределы вашего процесса.
Правило 2: Не мокируйте то чем вы владеете
# ❌ ПЛОХО — мокируем свой код
@patch("src.calculator.add")
def test_calculator(mock_add):
mock_add.return_value = 5
result = Calculator().compute(2, 3)
# Тестируем мок, не Calculator!
# ✅ ХОРОШО — тестируем реальный код
def test_calculator():
result = Calculator().compute(2, 3)
assert result == 5Правило 3: Integration vs Unit тесты
Unit-тесты: Тестируют одну функцию/класс изолированно
# Мокируем всё кроме тестируемого класса
def test_order_service_unit(mock_db, mock_email):
service = OrderService(db=mock_db, email=mock_email)
service.create_order(user_id=1, items=[...])
mock_db.save.assert_called_once()
mock_email.send.assert_called_once()Integration-тесты: Тестируют взаимодействие нескольких компонентов
# Минимум моков (только внешние системы)
def test_order_service_integration(fake_db):
email_service = RealEmailService()
service = OrderService(db=fake_db, email=email_service)
order = service.create_order(user_id=1, items=[...])
# Проверяем реальное взаимодействие
assert fake_db.get_order(order.id) is not NoneБаланс: 70% unit (с моками) + 30% integration (минимум моков)
Что вы изучили
- Проблемы без моков — медленные, ненадёжные, непредсказуемые тесты
- Test doubles — mock, stub, fake, spy
- Когда мокировать — HTTP, файлы, время, БД, медленные операции
- Когда НЕ мокировать — чистая логика, простые объекты, свой код
- Практические guidelines — мокируйте границы системы
Следующий урок
Отлично! Теперь вы понимаете зачем нужны моки. Но как их создавать в Python?
Переходите к уроку 1: unittest.mock: Mock, patch, MagicMock
В следующем уроке вы узнаете:
Mock()для создания моков@patch()для подмены зависимостейMagicMockдля magic methods- Проверка вызовов:
assert_called_once(),assert_called_with()
Устранение неисправностей
- Unit (с моками): Быстрые, проверяют логику изолированно (70% тестов)
- Integration (минимум моков): Проверяют что всё работает вместе (30% тестов)
- Stub: Простые данные, не проверяем вызовы (большинство случаев)
- Mock: Проверяем СКОЛЬКО раз и С КАКИМИ параметрами вызвали (когда важно поведение)