← Назад к вопросам

Как свести 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 делает их хрупкими и сложными в поддержке.