unittest.mock: Mock, patch, MagicMock
У вас есть функция send_welcome_email(user) которая вызывает SMTP-сервер. Как протестировать БЕЗ реальной отправки email?
Цель: Научиться создавать моки с unittest.mock и подменять зависимости в тестах.
Вы точно готовы?
Убедитесь, что прошли предыдущий урок:
- ✅ Понимаете зачем нужны моки
- ✅ Знаете разницу между mock/stub/fake
- ✅ Знаете когда использовать мокирование
Если непонятно — вернитесь к Мокирование: зачем и когда.
Проблема: как подменить зависимость?
Код с внешней зависимостью
# src/email_service.py
import smtplib
def send_email(to, subject, body):
"""Отправляет email через SMTP"""
server = smtplib.SMTP("smtp.gmail.com", 587)
server.starttls()
server.login("user@gmail.com", "password")
server.sendmail("user@gmail.com", to, f"Subject: {subject}\n\n{body}")
server.quit()
return TrueКак протестировать?
# ❌ Плохой тест — реально отправляет email!
def test_send_email():
result = send_email("test@example.com", "Hi", "Hello!")
assert result == True
# Email отправлен на реальный адрес!Проблемы:
- ❌ Каждый запуск теста отправляет реальный email
- ❌ Нужны реальные credentials
- ❌ Медленно (сетевой запрос)
Что нужно: Подменить smtplib.SMTP на мок-объект.
Решение 1: Mock() — создание мок-объектов
Базовый Mock
from unittest.mock import Mock
# Создаём мок-объект
mock_server = Mock()
# Мок отвечает на ЛЮБОЙ вызов метода
mock_server.starttls() # OK
mock_server.login("a", "b") # OK
mock_server.sendmail() # OK
mock_server.any_method() # OK — мок принимает всё!
# Проверяем что методы были вызваны
mock_server.starttls.assert_called_once()
mock_server.login.assert_called_once_with("a", "b")✅ Mock-объект записывает все вызовы и позволяет их проверять!
return_value: что возвращать
# Мок возвращает другой мок по умолчанию
mock = Mock()
result = mock.some_method()
print(result) # <Mock name='mock.some_method()' id='...'>
# Установим конкретное значение
mock.some_method.return_value = 42
result = mock.some_method()
print(result) # 42Практический пример:
# src/weather_service.py
import requests
class WeatherService:
def get_temperature(self, city):
response = requests.get(f"https://api.weather.com/?city={city}")
data = response.json()
return data["temperature"]
# tests/test_weather.py
from unittest.mock import Mock
def test_get_temperature():
# Arrange: создаём мок requests
mock_response = Mock()
mock_response.json.return_value = {"temperature": 15}
mock_requests = Mock()
mock_requests.get.return_value = mock_response
# Act: подменяем requests на мок (пока руками)
service = WeatherService()
# Как подменить requests? См. @patch ниже!side_effect: динамическое поведение
# 1. Вызвать исключение
mock = Mock()
mock.some_method.side_effect = ValueError("Boom!")
mock.some_method() # Выбросит ValueError
# 2. Вернуть разные значения при каждом вызове
mock.some_method.side_effect = [10, 20, 30]
print(mock.some_method()) # 10
print(mock.some_method()) # 20
print(mock.some_method()) # 30
# 3. Использовать функцию
def custom_logic(arg):
return arg * 2
mock.some_method.side_effect = custom_logic
print(mock.some_method(5)) # 10
print(mock.some_method(10)) # 20Практический пример:
# Мокируем API который падает с ошибкой
def test_api_timeout():
mock_api = Mock()
mock_api.fetch_data.side_effect = TimeoutError("API timeout")
service = DataService(api=mock_api)
with pytest.raises(TimeoutError):
service.get_data()Решение 2: @patch() — подмена зависимостей
Проблема: как подменить импорт
# src/weather.py
import requests # Как подменить этот import?
def get_temperature(city):
response = requests.get(f"https://api.weather.com/?city={city}")
return response.json()["temperature"]Решение: @patch()
# tests/test_weather.py
from unittest.mock import patch
@patch("src.weather.requests") # Подменяем requests в модуле src.weather
def test_get_temperature(mock_requests):
"""Тест с подменой requests"""
# Arrange: настраиваем мок
mock_response = Mock()
mock_response.json.return_value = {"temperature": 15}
mock_requests.get.return_value = mock_response
# Act: вызываем функцию
temp = get_temperature("Moscow")
# Assert
assert temp == 15
mock_requests.get.assert_called_once_with("https://api.weather.com/?city=Moscow")✅ @patch() автоматически подменяет requests на мок!
Правило: где патчить
Важно: Патчите ТАМ где объект ИСПОЛЬЗУЕТСЯ, не где определён!
# src/email_service.py
import smtplib # Импорт здесь
def send_email(to, subject, body):
server = smtplib.SMTP(...) # Используется здесь
# ...Правильно:
# ✅ Патчим в модуле где используется
@patch("src.email_service.smtplib")
def test_send_email(mock_smtplib):
passНеправильно:
# ❌ Патчим в модуле где определён
@patch("smtplib") # Не сработает!
def test_send_email(mock_smtplib):
passМножественный патчинг
@patch("src.service.requests")
@patch("src.service.open")
def test_service(mock_open, mock_requests):
# Порядок аргументов ОБРАТНЫЙ порядку декораторов!
# mock_open — последний декоратор (второй сверху)
# mock_requests — первый декоратор (первый сверху)
passПроще использовать контекстный менеджер:
def test_service():
with patch("src.service.requests") as mock_requests, \
patch("src.service.open") as mock_open:
# Порядок аргументов естественный
mock_requests.get.return_value = Mock(json=lambda: {"data": 123})
mock_open.return_value = Mock()
# Тест...Практический пример: EmailService
# src/email_service.py
import smtplib
def send_email(to, subject, body):
"""Отправляет email через SMTP"""
server = smtplib.SMTP("smtp.gmail.com", 587)
server.starttls()
server.login("user@gmail.com", "password")
server.sendmail("user@gmail.com", to, f"Subject: {subject}\n\n{body}")
server.quit()
return True
# tests/test_email_service.py
from unittest.mock import patch, Mock
@patch("src.email_service.smtplib.SMTP")
def test_send_email(mock_smtp_class):
"""Тест отправки email БЕЗ реального SMTP"""
# Arrange: настраиваем мок SMTP сервера
mock_server = Mock()
mock_smtp_class.return_value = mock_server # SMTP(...) вернёт наш мок
# Act: вызываем функцию
result = send_email("test@example.com", "Hello", "Test email")
# Assert: проверяем результат
assert result == True
# Assert: проверяем что SMTP вызван правильно
mock_smtp_class.assert_called_once_with("smtp.gmail.com", 587)
mock_server.starttls.assert_called_once()
mock_server.login.assert_called_once_with("user@gmail.com", "password")
mock_server.sendmail.assert_called_once()
mock_server.quit.assert_called_once()✅ Email НЕ отправлен, но мы проверили что код вызывает правильные методы!
Проверка вызовов (verification)
Базовые методы
mock = Mock()
mock.method(1, 2, key="value")
# Проверка что вызван хотя бы раз
mock.method.assert_called()
# Проверка что вызван РОВНО один раз
mock.method.assert_called_once()
# Проверка последнего вызова с конкретными аргументами
mock.method.assert_called_with(1, 2, key="value")
# Проверка что вызван РОВНО один раз с конкретными аргументами
mock.method.assert_called_once_with(1, 2, key="value")
# Проверка что НЕ вызван
mock.method.assert_not_called()Продвинутые проверки
mock = Mock()
mock.method(1, 2)
mock.method(3, 4)
# Получить количество вызовов
print(mock.method.call_count) # 2
# Получить все вызовы
print(mock.method.call_args_list)
# [call(1, 2), call(3, 4)]
# Проверить любой вызов (ANY)
from unittest.mock import ANY
mock.method.assert_called_with(1, ANY) # Второй аргумент любойПрактический пример
@patch("src.logger.Logger")
def test_service_logs_errors(mock_logger_class):
"""Проверяем что сервис логирует ошибки"""
mock_logger = Mock()
mock_logger_class.return_value = mock_logger
service = Service()
service.process_invalid_data({"bad": "data"})
# Проверяем что logger.error был вызван
mock_logger.error.assert_called_once()
# Проверяем сообщение об ошибке
call_args = mock_logger.error.call_args
message = call_args[0][0] # Первый позиционный аргумент
assert "Invalid data" in messageMagicMock: для магических методов (bonus)
Проблема с обычным Mock
mock = Mock()
# Магические методы не работают!
len(mock) # TypeError
mock[0] # TypeErrorРешение: MagicMock
from unittest.mock import MagicMock
mock = MagicMock()
# Магические методы работают!
mock.__len__.return_value = 5
print(len(mock)) # 5
mock.__getitem__.return_value = "value"
print(mock[0]) # "value"Практический пример:
@patch("src.service.open", MagicMock())
def test_read_file(mock_open):
"""Тест чтения файла"""
# Настраиваем мок файла с контекстным менеджером
mock_file = MagicMock()
mock_file.__enter__.return_value.read.return_value = "file content"
mock_open.return_value = mock_file
# Код использует: with open(...) as f: content = f.read()
content = read_config_file("config.txt")
assert content == "file content"Когда использовать:
Mock()— в 95% случаев (достаточно)MagicMock()— когда нужны__len__,__getitem__,__enter__и т.д.
Практический пример: полный тест
# src/order_service.py
import requests
from datetime import datetime
class OrderService:
def __init__(self, api_url):
self.api_url = api_url
def create_order(self, user_id, items):
"""Создаёт заказ через API"""
# 1. Получаем данные пользователя
response = requests.get(f"{self.api_url}/users/{user_id}")
user = response.json()
# 2. Создаём заказ
order_data = {
"user": user,
"items": items,
"created_at": datetime.now().isoformat()
}
response = requests.post(f"{self.api_url}/orders", json=order_data)
return response.json()
# tests/test_order_service.py
from unittest.mock import patch, Mock
import pytest
@patch("src.order_service.datetime")
@patch("src.order_service.requests")
def test_create_order(mock_requests, mock_datetime):
"""Тест создания заказа БЕЗ реального API"""
# Arrange: мокируем requests.get (получение пользователя)
mock_user_response = Mock()
mock_user_response.json.return_value = {
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
# Arrange: мокируем requests.post (создание заказа)
mock_order_response = Mock()
mock_order_response.json.return_value = {
"id": 101,
"status": "created"
}
# requests.get/post возвращают разные моки
mock_requests.get.return_value = mock_user_response
mock_requests.post.return_value = mock_order_response
# Arrange: мокируем datetime.now()
mock_datetime.now.return_value.isoformat.return_value = "2025-01-15T10:00:00"
# Act: создаём заказ
service = OrderService(api_url="https://api.example.com")
order = service.create_order(user_id=1, items=["item1", "item2"])
# Assert: проверяем результат
assert order["id"] == 101
assert order["status"] == "created"
# Assert: проверяем что API вызван правильно
mock_requests.get.assert_called_once_with("https://api.example.com/users/1")
mock_requests.post.assert_called_once()
# Assert: проверяем данные отправленные в POST
call_args = mock_requests.post.call_args
sent_data = call_args[1]["json"] # Именованный аргумент 'json'
assert sent_data["user"]["name"] == "Alice"
assert sent_data["items"] == ["item1", "item2"]
assert sent_data["created_at"] == "2025-01-15T10:00:00"✅ Тест работает мгновенно БЕЗ реального API!
Что вы изучили
- Mock() — создание мок-объектов
- return_value — что возвращать при вызове
- side_effect — динамическое поведение (исключения, последовательности)
- @patch() — подмена зависимостей в тестах
- Verification — assert_called_once_with(), call_count, call_args
- MagicMock — для магических методов (len, getitem)
Следующий урок
Отлично! Теперь вы умеете создавать моки и патчить зависимости. Но работать с HTTP-запросами через patch("requests") неудобно. Есть специализированные библиотеки!
Переходите к уроку 2: Мокирование HTTP: requests-mock
В следующем уроке вы узнаете:
requests-mock— удобное мокирование HTTP- Мокирование GET/POST/PUT/DELETE запросов
- Проверка headers, query params, JSON body
- Симуляция ошибок (timeout, 404, 500)
Устранение неисправностей
Проверьте что:
- Патчите правильное место:
@patch("src.module.dependency") - Мок настроен ДО вызова функции
- Функция действительно вызывает этот метод
Порядок аргументов ОБРАТНЫЙ порядку декораторов:
@patch("module.dep1") # Второй аргумент
@patch("module.dep2") # Первый аргумент
def test(mock_dep2, mock_dep1):
passИли используйте with patch(...) as mock: для понятного порядка.
Mock принимает ЛЮБОЙ вызов. Эта ошибка значит что вы обращаетесь к атрибуту мока ДО его вызова:
mock.method.return_value = 42 # ✅ OK
print(mock.result) # ❌ Mock не знает что этоСлишком много моков! Добавьте integration-тесты без моков чтобы проверить реальное взаимодействие компонентов.