Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Роль Mock в тестировании
Mock (мок-объект) — это подделка реального объекта, используемая в тестировании. Мок позволяет заменить зависимость реального кода на контролируемую версию, что даёт множество преимуществ при написании юнит-тестов.
Основная идея
# Без mock - тест зависит от реальной БД
def test_get_user_without_mock():
# Проблемы:
# - Тест медленный (обращается в реальную БД)
# - Нужна тестовая БД с данными
# - Тест нестабилен (может упасть если БД не доступна)
user = db.get_user(1) # Реальный запрос!
assert user.name == "Alice"
# С mock - тест изолирован
from unittest.mock import Mock
def test_get_user_with_mock():
# Преимущества:
# - Тест быстрый (не обращается в реальную БД)
# - Не нужна тестовая БД
# - Тест стабилен (полный контроль над зависимостями)
mock_db = Mock()
mock_db.get_user.return_value = User(id=1, name="Alice")
user = mock_db.get_user(1)
assert user.name == "Alice"
assert mock_db.get_user.called # Проверяем, что функция была вызвана
Основные преимущества Mock
1. Изоляция кода от зависимостей
class PaymentService:
def __init__(self, payment_gateway):
self.gateway = payment_gateway
def charge(self, user_id: int, amount: float):
# Хотим тестировать логику charging
# Но НЕ хотим обращаться к реальному payment gateway
result = self.gateway.process_payment(user_id, amount)
if result.success:
self.log_success(user_id, amount)
else:
self.log_failure(user_id, amount)
return result
# Тест с mock
from unittest.mock import Mock
def test_charge_success():
# Создаём мок gateway вместо реального (Stripe, PayPal)
mock_gateway = Mock()
mock_gateway.process_payment.return_value = Mock(success=True)
service = PaymentService(mock_gateway)
result = service.charge(user_id=1, amount=100.0)
assert result.success
mock_gateway.process_payment.assert_called_once_with(1, 100.0)
2. Контроль над внешними сервисами
class WeatherService:
def __init__(self, http_client):
self.client = http_client
def get_temperature(self, city: str) -> float:
response = self.client.get(f"https://api.weather.com/{city}")
if response.status_code == 200:
return response.json()["temperature"]
raise WeatherAPIError("Failed to fetch weather")
def test_get_temperature_success():
# Тестируем успешный сценарий
mock_client = Mock()
mock_response = Mock(status_code=200, json=lambda: {"temperature": 25})
mock_client.get.return_value = mock_response
service = WeatherService(mock_client)
temp = service.get_temperature("Moscow")
assert temp == 25
mock_client.get.assert_called_once_with("https://api.weather.com/Moscow")
def test_get_temperature_error():
# Тестируем обработку ошибки
mock_client = Mock()
mock_client.get.return_value = Mock(status_code=500)
service = WeatherService(mock_client)
with pytest.raises(WeatherAPIError):
service.get_temperature("London")
3. Скорость тестов
# Без mock - тест занимает 2-5 секунд (реальный API запрос)
def test_without_mock():
response = requests.get("https://api.example.com/data") # Медленно!
assert response.status_code == 200
# С mock - тест выполняется за миллисекунды
from unittest.mock import patch
@patch(requests.get) # Заменяем реальный requests.get на mock
def test_with_mock(mock_get):
mock_get.return_value.status_code = 200 # Быстро!
response = requests.get("https://api.example.com/data")
assert response.status_code == 200
4. Тестирование обработки ошибок
class DataProcessor:
def __init__(self, db):
self.db = db
def process_data(self, data_id: int):
try:
data = self.db.get_data(data_id)
processed = self.process(data)
self.db.save(processed)
return processed
except DatabaseError as e:
self.log_error(f"DB error: {e}")
raise
def test_process_data_with_db_error():
# Тестируем, что система правильно обрабатывает ошибки БД
mock_db = Mock()
mock_db.get_data.side_effect = DatabaseError("Connection lost")
processor = DataProcessor(mock_db)
with pytest.raises(DatabaseError):
processor.process_data(1)
# Проверяем, что ошибка была залогирована
processor.log_error.assert_called_once()
5. Проверка взаимодействий между объектами
class OrderService:
def __init__(self, db, email_service, payment_service):
self.db = db
self.email = email_service
self.payment = payment_service
def place_order(self, user_id: int, items: list):
# Создаём заказ
order = self.db.create_order(user_id, items)
# Обрабатываем платёж
payment = self.payment.charge(user_id, order.total)
# Отправляем письмо подтверждения
self.email.send_confirmation(user_id, order)
return order
def test_place_order():
mock_db = Mock()
mock_email = Mock()
mock_payment = Mock()
mock_db.create_order.return_value = Order(id=1, total=100.0)
mock_payment.charge.return_value = Payment(success=True)
service = OrderService(mock_db, mock_email, mock_payment)
order = service.place_order(user_id=1, items=["item1"])
# Проверяем, что все зависимости были вызваны в правильном порядке
mock_db.create_order.assert_called_once_with(1, ["item1"])
mock_payment.charge.assert_called_once_with(1, 100.0)
mock_email.send_confirmation.assert_called_once_with(1, order)
Типы Mock объектов
1. Mock — базовый мок, записывает все вызовы
from unittest.mock import Mock
mock = Mock()
mock.some_method()
assert mock.some_method.called
2. MagicMock — мок с поддержкой магических методов
from unittest.mock import MagicMock
mock_list = MagicMock()
mock_list[0] = "first" # Поддерживает __getitem__
assert len(mock_list) >= 1
3. patch — заменяет объект в определённой области кода
from unittest.mock import patch
@patch(module.ClassName) # Заменяет на время теста
def test_something(mock_class):
mock_class.return_value = Mock(attr="value")
4. PropertyMock — мок для properties
from unittest.mock import PropertyMock
with patch.object(type(obj), property_name, PropertyMock(return_value=42)):
assert obj.property_name == 42
Реальный пример: Async код с pytest-mock
import pytest
from unittest.mock import AsyncMock
class UserRepository:
async def get_user(self, user_id: int):
# Асинхронный запрос в БД
pass
class UserService:
def __init__(self, repo):
self.repo = repo
async def get_user_name(self, user_id: int):
user = await self.repo.get_user(user_id)
return user.name
@pytest.mark.asyncio
async def test_get_user_name():
mock_repo = AsyncMock(spec=UserRepository)
mock_repo.get_user.return_value = Mock(name="Alice")
service = UserService(mock_repo)
name = await service.get_user_name(1)
assert name == "Alice"
mock_repo.get_user.assert_called_once_with(1)
Антипаттерны (чего избегать)
# ПЛОХО - мокируем слишком много
def test_calculation():
mock_math = Mock() # Зачем мокировать math?
# Тест теряет смысл
# ХОРОШО - мокируем только внешние зависимости
def test_calculation():
mock_api = Mock() # Мокируем API
# Реальная логика остаётся реальной
# ПЛОХО - мокируем всю логику
def test_service():
mock_service = Mock() # Мок сервиса тестирует самого себя?!
# Бесполезный тест
# ХОРОШО - тестируем реальную логику с замоканными зависимостями
def test_service():
mock_db = Mock() # Только зависимость
service = RealService(mock_db) # Реальный сервис
Итог
Mock даёт:
✅ Скорость — тесты выполняются быстро ✅ Надёжность — тесты не зависят от внешних сервисов ✅ Контроль — полный контроль над поведением зависимостей ✅ Изоляция — каждый тест тестирует только одно ✅ Проверка взаимодействий — убеждаемся, что компоненты работают вместе правильно
Mock — это не просто удобство, это необходимость для написания качественных юнит-тестов.