🎭 Мокинг: тестируем код с API без настоящего API
⚠️ Урок устарел. Свежая версия в курсе 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 False4. База данных
Мокаем 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)
Поздравляю! Теперь ваши тесты работают всегда и везде — без внешних сюрпризов. 🎯