Перейти к содержимому
К программе курса
Pytest с нуля: тесты, которые реально работают
9 / 1182%

🎭 Мокинг: тестируем код с API без настоящего API

30 минут

⚠️ Урок устарел. Свежая версия в курсе pytest-junior: unittest.mock: Mock, patch, MagicMock.

Мокинг: тесты, которые работают всегда

🌐 Проблема: тесты падают без интернета, API медленные или меняют формат
Решение: моки изолируют тесты — быстрые, стабильные, без внешних зависимостей

✅ Тестируем код с API без реальных HTTP запросов
✅ Работаем с файлами и временем без реальных файлов и часов
✅ Тесты проходят в самолёте, без сети и лимитов

Зачем нужны моки? Реальная боль новичков

Если тест зависит от внешнего мира (интернет, API, файлы), он будет падать из-за вещей вне вашего контроля: нет сети, сервис лежит, формат ответа изменился, закончились лимиты.

# ❌ ТЕСТ, КОТОРЫЙ ПАДАЕТ ВСЮДУ
def test_fetch_user_data():
    response = requests.get("https://api.example.com/users/1")  # зависит от сети и API
    data = response.json()
    assert data["name"] == "John"

Моки заменяют внешнюю зависимость на предсказуемую заглушку: тест выполняется мгновенно и стабильно, даже без интернета.

# ✅ ТЕСТ С МОКАМИ — РАБОТАЕТ ВСЕГДА
from unittest.mock import patch
 
def test_fetch_user_data_with_mock():
    with patch("requests.get") as mock_get:
        mock_get.return_value.json.return_value = {
            "id": 1,
            "name": "John",
            "email": "john@example.com",
        }
        user = fetch_user_data(1)
 
        assert user.name == "John"
        mock_get.assert_called_once_with("https://api.example.com/users/1")

unittest.mock.patch — основной инструмент

patch временно подменяет импортируемые функции/объекты и автоматически восстанавливает их после блока или теста.

patch контекстно (блок кода)

def fetch_data():
    resp = requests.get("https://api.example.com/data", timeout=5)
    resp.raise_for_status()
    return resp.json()
 
from unittest.mock import patch
import requests
 
def test_api_call():
    with patch("requests.get") as mock_get:
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {"data": "test"}
 
        result = fetch_data()
 
        assert result == {"data": "test"}
        mock_get.assert_called_once_with("https://api.example.com/data")

patch на весь тест (декоратор) и обработка исключений

Декоратор заменяет зависимость на весь тест; side_effect позволяет эмулировать ошибки или разные ответы.

def fetch_data():
    resp = requests.get("https://api.example.com/data", timeout=5)
    resp.raise_for_status()
    return resp.json()
 
# patch как декоратор
@patch("requests.get")
def test_api_call_with_decorator(mock_get):
    mock_get.return_value.json.return_value = {"data": "test"}
    result = fetch_data()
    assert result == {"data": "test"}
 
# Моки с исключениями
def test_api_error_handling():
    with patch("requests.get") as mock_get:
        mock_get.side_effect = requests.RequestException("Network error")
        with pytest.raises(DataFetchError):
            fetch_data()

monkeypatch — встроенный в pytest

monkeypatch удобен для простых подмен: атрибутов, env переменных, функций. После теста всё восстанавливается автоматически.

Простая подмена функций

Подменяем requests.get на свою функцию, чтобы не ходить в сеть. Фикстура monkeypatch автоматически доступна во всех тестах pytest и откатывает изменения после выполнения.

def test_with_monkeypatch(monkeypatch):
    def fake_get(url):
        return {"mocked": True}
 
    monkeypatch.setattr(requests, "get", fake_get)
 
    result = fetch_data()
    assert result["mocked"] is True
    # 🎯 Автоматическое восстановление после теста

Подмена переменных окружения

Используем setenv, чтобы код читал нужные значения без правки реальных переменных окружения.

def test_env_variables(monkeypatch):
    monkeypatch.setenv("API_KEY", "test-key")
    config = load_config()
    assert config.api_key == "test-key"

Подмена атрибутов объектов

Можно подменять методы/флаги на уже созданных объектах для проверки разных состояний.

def test_object_attributes(monkeypatch):
    user = User(name="test")
    monkeypatch.setattr(user, "is_admin", lambda: True)
    assert user.is_admin() is True

Реальные сценарии мокинга

Эти примеры показывают типичные задачи новичков: HTTP вызовы, работа с файлами, временем и БД без реальных зависимостей.

1. HTTP запросы

Заменяем вызовы HTTP-клиента на заглушки, чтобы не зависеть от сети и формата ответа.

@patch("httpx.get")
def test_fetch_weather(mock_get):
    mock_get.return_value.json.return_value = {
        "temperature": 25,
        "condition": "sunny",
        "city": "London",
    }
    weather = fetch_weather("London")
    assert weather.temperature == 25
    assert weather.condition == "sunny"

2. Работа с файлами

Подменяем open реальным временным файлом, чтобы не создавать/удалять файлы руками.

def test_process_config(tmp_path):
    config_file = tmp_path / "config.yaml"
    config_file.write_text("api_key: test-key\ndebug: true")
 
    # Подменяем open, чтобы load_config читал наш временный файл
    with patch("builtins.open", return_value=config_file.open()):
        config = load_config()
        assert config["api_key"] == "test-key"

3. Работа со временем

Мокаем datetime чтобы тестировать логику для любой даты/времени.

from unittest.mock import patch
import datetime
 
def test_is_weekend():
    with patch("datetime.datetime") as mock_datetime:
        mock_datetime.now.return_value.weekday.return_value = 5  # Суббота
        assert is_weekend() is True
        mock_datetime.now.return_value.weekday.return_value = 0  # Понедельник
        assert is_weekend() is False

4. База данных

Мокаем ORM-запрос, чтобы не поднимать реальную БД и проверять обработку данных.

@patch("database.models.User.query")
def test_get_user_by_email(mock_query):
    mock_user = Mock()
    mock_user.email = "test@example.com"
    mock_query.filter_by.return_value.first.return_value = mock_user
 
    user = get_user_by_email("test@example.com")
    assert user.email == "test@example.com"

patch vs monkeypatch: когда что использовать

  • patch (unittest.mock) — удобно для модульных импортов, контекстных менеджеров и декораторов; даёт методы assert_called_with, side_effect.
  • monkeypatch (pytest) — быстрое внедрение зависимостей (setattr, setenv), автоклейнап; проще для новичков.

❌ Анти-паттерны: как НЕ использовать моки

Мок всего подряд

def test_complex_function():
    with patch("module.api.call"), \
         patch("module.database.query"), \
         patch("module.file.read"), \
         patch("module.config.get"), \
         patch("module.time.now"):
        result = complex_function()
    # 🚨 Тест проверяет моки, а не логику
    # Итог: сложно понять, что реально проверяется, тест хрупкий и бесполезный

Слишком строгие проверки

def test_over_specified():
    with patch("requests.get") as mock_get:
        fetch_data()
        mock_get.assert_called_once_with(
            "https://api.example.com/data",
            headers={"Authorization": "Bearer token"},
            timeout=30,
            params={"page": 1, "limit": 50},  # 🚨 хрупко
        )
        # Итог: малейшее изменение параметров ломает тест, хотя логика может быть корректной
def test_flexible():
    with patch("requests.get") as mock_get:
        fetch_data()
        args, kwargs = mock_get.call_args
        assert args[0].startswith("https://api.example.com")
        assert "Authorization" in kwargs.get("headers", {})

Мок приватной логики

def test_mock_internals():
    with patch("my_module._private_helper"):
        result = public_function()
        # Итог: тест проверяет детали реализации, не поведение
def test_public_interface():
    with patch("my_module.external_dependency"):
        result = public_function()

Практика: создайте свои первые моки

Задача 1: отправка email

def send_notification(email, message):
    import smtplib
    with smtplib.SMTP("smtp.example.com") as server:
        server.sendmail("noreply@example.com", email, f"Subject: Notification\n\n{message}")

Напишите тест с моком для smtplib.SMTP:

  • Проверить, что SMTP создаётся с правильным хостом
  • Проверить, что sendmail вызывается с нужными аргументами

Задача 2: функция с временем

def is_morning():
    from datetime import datetime
    hour = datetime.now().hour
    return 6 <= hour < 12

Напишите тест с моком для datetime:

  • Утро (возвращает True)
  • Вечер (возвращает False)

Подсказка: используйте patch для внешних вызовов (SMTP, datetime) и проверяйте, что код ведёт себя правильно без реальных сетевых/временных зависимостей.

🚀 Шпаргалка: моки в действии

Когда использовать моки

  • 🌐 HTTP запросы — тесты не должны зависеть от внешних API
  • 📁 Файлы — не создавайте реальные файлы в тестах
  • ⏰ Время — тестируйте логику для любого момента
  • 🗄️ Базы данных — изолируйте от реальной БД
  • 🔧 Внешние сервисы — email, SMS, платежи

Инструменты

Коротко о главных способах мокинга в Python/pytest:

# unittest.mock.patch
with patch("module.function") as mock:
    mock.return_value = "result"
 
# pytest monkeypatch
def test_example(monkeypatch):
    monkeypatch.setattr(module, "function", lambda: "result")
 
# Mock объекты
from unittest.mock import Mock
mock_obj = Mock()
mock_obj.method.return_value = "result"

Полезные методы mock объектов

# Возвращаемое значение
mock.return_value = {"status": "ok"}  # fetch_data() вернёт этот словарь
 
# Исключение или последовательность значений
mock.side_effect = TimeoutError()     # simulate timeout
mock.side_effect = [1, 2, 3]          # три вызова: 1, потом 2, потом 3
 
# Проверки вызовов
mock.assert_called_once()             # убедиться, что отправили email только один раз
mock.assert_called_with("url")        # убедиться, что запрос ушёл на правильный URL
mock.call_count                       # сколько раз дернули — например, ретраи

Чеклист качественных моков

  • Мокаем только внешние зависимости
  • Тесты изолированы — без сети/БД/файлов
  • Проверяем поведение, не только возвращаемые значения
  • Минимальный scope моков
  • Тесты быстрые — никаких реальных запросов
  • Восстанавливаем состояние (context manager/monkeypatch)

Поздравляю! Теперь ваши тесты работают всегда и везде — без внешних сюрпризов. 🎯