← Назад к вопросам
Как свести Mock объекты к минимуму?
2.0 Middle🔥 171 комментариев
#Архитектура и паттерны#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Минимизация Mock объектов в тестах
Избыток Mock объектов — частая проблема в unit тестировании. Тесты становятся хрупкими, сложными и дорогими в поддержке. Давайте разберёмся, как писать тесты правильно.
Проблема избытка мокирования
Плохой пример (слишком много Mock)
from unittest.mock import Mock, patch, MagicMock
class UserService:
def __init__(self, db, cache, logger, email_service):
self.db = db
self.cache = cache
self.logger = logger
self.email_service = email_service
def create_user(self, name, email):
user = self.db.create(name, email)
self.cache.set(f"user:{user.id}", user)
self.logger.info(f"User created: {user.id}")
self.email_service.send_welcome(user.email)
return user
# ❌ Плохой тест (слишком много Mock)
def test_create_user_bad():
mock_db = Mock()
mock_cache = Mock()
mock_logger = Mock()
mock_email = Mock()
service = UserService(mock_db, mock_cache, mock_logger, mock_email)
mock_user = Mock()
mock_user.id = 1
mock_user.email = "test@example.com"
mock_db.create.return_value = mock_user
result = service.create_user("John", "test@example.com")
# Слишком много утверждений
assert result == mock_user
mock_db.create.assert_called_once_with("John", "test@example.com")
mock_cache.set.assert_called_once_with("user:1", mock_user)
mock_logger.info.assert_called_once()
mock_email.send_welcome.assert_called_once_with("test@example.com")
# Проблемы:
# 1. Тест знает ВСЕ внутренние детали реализации
# 2. Любое малейшее изменение сломает тест
# 3. Тест проверяет "как" работает, а не "что" работает
1. Используйте реальные объекты вместо Mock
Замените Mock на реальные зависимости
from dataclasses import dataclass
from typing import Protocol
# Используем Protocol для типизации
class UserRepository(Protocol):
def create(self, name: str, email: str): ...
# Реальная реализация для тестов
class InMemoryUserRepository:
def __init__(self):
self.users = []
self.id_counter = 0
def create(self, name: str, email: str):
self.id_counter += 1
user = User(id=self.id_counter, name=name, email=email)
self.users.append(user)
return user
# Реальное логирование (не критично мокировать)
class TestLogger:
def __init__(self):
self.logs = []
def info(self, msg: str):
self.logs.append(msg)
# ✅ Лучший тест
def test_create_user_good():
# Используем реальные объекты
db = InMemoryUserRepository()
logger = TestLogger()
# Мокируем только КРИТИЧНЫЕ зависимости
cache = Mock() # Может быть медленным
email = Mock() # Имеет побочные эффекты
service = UserService(db, cache, logger, email)
result = service.create_user("John", "john@example.com")
# Проверяем ПОВЕДЕНИЕ, а не реализацию
assert result.id == 1
assert result.name == "John"
assert result.email == "john@example.com"
# Проверяем побочные эффекты только для важных зависимостей
cache.set.assert_called_once()
email.send_welcome.assert_called_once()
2. Принцип Mock только для I/O операций
Мокируйте ТОЛЬКО:
# ✅ Мокируйте эти зависимости
- HTTP клиенты (requests, httpx)
- Внешние API (PayPal, Stripe, OpenAI)
- Отправку email/SMS
- Работу с файловой системой
- Работу с базой данных (если есть реальная БД)
- Redis/кэши
- Очереди сообщений
- Внешние сервисы
# ❌ НЕ мокируйте
- Утилиты и Helper функции
- Бизнес-логику
- Валидацию
- Вычисления
3. Используйте Fixtures для переиспользуемых Mock
Плохо: дублирование Mock кода
def test_send_email_1():
mock_smtp = Mock()
mock_smtp.send.return_value = True
# Много кода
def test_send_email_2():
mock_smtp = Mock()
mock_smtp.send.return_value = True
# Дублирование
Хорошо: Fixture
import pytest
from unittest.mock import Mock
@pytest.fixture
def mock_email_service():
"""Переиспользуемый Mock для email сервиса"""
mock = Mock()
mock.send.return_value = True
return mock
def test_send_email_1(mock_email_service):
# Используем fixture
result = mock_email_service.send("test@example.com", "Hello")
assert result is True
def test_send_email_2(mock_email_service):
# Переиспользуем
result = mock_email_service.send("other@example.com", "Hi")
assert result is True
4. Используйте фальшивые реализации (Stub/Fake)
Вместо Mock — создайте простую реальную реализацию
class FakePaymentGateway:
"""Реальная реализация для тестов"""
def __init__(self):
self.charges = []
self.should_fail = False
def charge(self, amount: float, card_token: str) -> str:
if self.should_fail:
raise PaymentError("Payment failed")
charge_id = f"CHARGE_{len(self.charges) + 1}"
self.charges.append({
'id': charge_id,
'amount': amount,
'card': card_token
})
return charge_id
# Использование
def test_order_with_payment():
gateway = FakePaymentGateway()
service = OrderService(gateway)
order_id = service.create_order(100, "tok_123")
assert order_id is not None
assert len(gateway.charges) == 1
assert gateway.charges[0]['amount'] == 100
def test_order_payment_failure():
gateway = FakePaymentGateway()
gateway.should_fail = True
service = OrderService(gateway)
with pytest.raises(PaymentError):
service.create_order(100, "tok_invalid")
5. Используйте VCR для HTTP запросов
Мокирование HTTP запросов с записью ответов
import vcr
import requests
# Записывает реальные ответы при первом запуске
@vcr.use_cassette('fixtures/get_user.yaml')
def test_get_user_from_api():
response = requests.get('https://api.example.com/users/1')
data = response.json()
assert data['id'] == 1
assert data['name'] == 'John'
# При повторных запусках используется записанный ответ
# Тест быстрый и не требует реального API
6. Используйте параметризацию вместо дублирования Mock
Плохо: много похожих тестов
def test_validate_email_valid():
validator = EmailValidator()
assert validator.is_valid("test@example.com") is True
def test_validate_email_another():
validator = EmailValidator()
assert validator.is_valid("user@domain.co.uk") is True
def test_validate_email_invalid():
validator = EmailValidator()
assert validator.is_valid("not-an-email") is False
Хорошо: параметризация
@pytest.mark.parametrize("email,expected", [
("test@example.com", True),
("user@domain.co.uk", True),
("not-an-email", False),
("", False),
("@example.com", False),
])
def test_validate_email(email, expected):
validator = EmailValidator()
assert validator.is_valid(email) is expected
7. Проверяйте поведение, а не реализацию
Плохо: тест зависит от реализации
# ❌ Проверяет ДЕТАЛЬ реализации (как)
def test_cache_set():
cache = CacheService(redis_client)
cache.set("key", "value")
# Проверяем вызов Redis напрямую (деталь реализации)
redis_client.set.assert_called_once_with("key", "value")
Хорошо: проверяем поведение
# ✅ Проверяет ПОВЕДЕНИЕ (что)
def test_cache_retrieval():
cache = CacheService(redis_client)
cache.set("key", "value")
retrieved = cache.get("key")
# Проверяем результат, не реализацию
assert retrieved == "value"
8. Dependency Injection для облегчения тестирования
Плохо: сложно тестировать
class UserService:
def __init__(self):
self.db = UserRepository() # Жёсткая зависимость
self.cache = RedisCache() # Жёсткая зависимость
def create_user(self, name):
return self.db.create(name)
Хорошо: Dependency Injection
class UserService:
def __init__(self, db: UserRepository, cache: CacheService):
self.db = db # Инъецируется
self.cache = cache # Инъецируется
def create_user(self, name):
return self.db.create(name)
# Тестирование с реальными объектами
def test_create_user():
db = InMemoryRepository()
cache = InMemoryCache()
service = UserService(db, cache)
user = service.create_user("John")
assert user.name == "John"
Чеклист минимизации Mock
# 1. Используйте реальные объекты по умолчанию
✅ db = InMemoryRepository()
❌ db = Mock()
# 2. Мокируйте только I/O операции
✅ mock_http_client
❌ mock_validator
# 3. Используйте Fake вместо Mock где возможно
✅ FakePaymentGateway
❌ Mock()
# 4. Проверяйте поведение, не реализацию
✅ assert result.id == 1
❌ mock.create.assert_called_once_with(...)
# 5. Используйте Fixtures для переиспользования
✅ @pytest.fixture def mock_service()
❌ mock_service = Mock() в каждом тесте
# 6. Параметризуйте похожие тесты
✅ @pytest.mark.parametrize
❌ def test_1, test_2, test_3
# 7. Dependency Injection
✅ service = UserService(db, cache)
❌ service = UserService() с жёсткими зависимостями
# 8. VCR для HTTP
✅ @vcr.use_cassette('fixture.yaml')
❌ Mock(requests.get)
Практический пример: правильный подход
import pytest
from unittest.mock import Mock
# 1. Реальная реализация для тестов
class FakeUserRepository:
def __init__(self):
self.users = {}
self.id_counter = 0
def create(self, name, email):
self.id_counter += 1
user = {'id': self.id_counter, 'name': name, 'email': email}
self.users[self.id_counter] = user
return user
# 2. Fixture для Mock (только для I/O)
@pytest.fixture
def mock_email_service():
return Mock()
# 3. Тест с минимальным мокированием
def test_user_creation(mock_email_service):
db = FakeUserRepository()
service = UserService(db, mock_email_service)
user = service.create_user("Alice", "alice@example.com")
# Проверяем поведение
assert user['id'] == 1
assert user['name'] == "Alice"
# Мокируем только I/O (email)
mock_email_service.send.assert_called_once()
Итоговые рекомендации
- Минимизируйте Mock — используйте реальные объекты по умолчанию
- Мокируйте только I/O — HTTP, email, БД, файловая система
- Используйте Fake — простые реальные реализации для тестов
- Проверяйте поведение — результаты, а не детали реализации
- Dependency Injection — легче подменять зависимости
- VCR для HTTP — запишите ответы, используйте кэш
- Fixtures и параметризация — избегайте дублирования
Тесты должны быть простыми, быстрыми и стабильными. Избыток Mock делает их хрупкими и сложными в поддержке.