Skip to main content
Back to course
Pytest для джунов: Моки и интеграция
1 / 813%

Мокирование: зачем и когда

15 минут

Вы написали 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"}
    # Тест без реального HTTP

2. Файловый ввод-вывод

# Мокируем 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 == 200

3. Простые объекты (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()

Устранение неисправностей

Моки полезны для внешних зависимостей (HTTP, БД). Хрупкими становятся тесты которые мокируют внутреннюю логику — это anti-pattern. Мокируйте границы системы, тестируйте бизнес-логику реально.
  • Unit (с моками): Быстрые, проверяют логику изолированно (70% тестов)
  • Integration (минимум моков): Проверяют что всё работает вместе (30% тестов)
  • Stub: Простые данные, не проверяем вызовы (большинство случаев)
  • Mock: Проверяем СКОЛЬКО раз и С КАКИМИ параметрами вызвали (когда важно поведение)
Мокирование: зачем и когда — Pytest для джунов: Моки и интеграция — Potapov.me