Что такое mock-объекты и как их использовать?python-59

Mock-объекты (или "моки") — это специальные объекты-заглушки, которые имитируют поведение реальных объектов в тестах. Они позволяют изолировать тестируемый код от внешних зависимостей.

Основные понятия

  1. Mock - базовый объект-заглушка
  2. MagicMock - расширенный mock с поддержкой магических методов
  3. Patch - временная замена реального объекта mock-ом
  4. Spec - ограничение mock-а интерфейсом реального объекта

Зачем нужны mock-объекты?

  • Тестирование в изоляции (без внешних API, БД и т.д.)
  • Контроль возвращаемых значений
  • Проверка вызовов (был ли вызван метод, с какими аргументами)
  • Упрощение сложных тестовых сценариев

Базовое использование

Стандартный модуль unittest.mock (входит в стандартную библиотеку Python):

from unittest.mock import Mock

# Создание mock-объекта
mock = Mock()

# Установка возвращаемого значения
mock.some_method.return_value = 42

# Вызов метода
result = mock.some_method()

assert result == 42

MagicMock

from unittest.mock import MagicMock

mock_dict = MagicMock()
mock_dict.__getitem__.return_value = "value"

assert mock_dict["key"] == "value"

Проверка вызовов

mock = Mock()
mock.method(1, 2, 3, key="value")

# Проверка что метод был вызван
mock.method.assert_called()
# С конкретными аргументами
mock.method.assert_called_with(1, 2, 3, key="value")
# Сколько раз вызывался
assert mock.method.call_count == 1

Patch

Чаще всего используется через декоратор или контекстный менеджер:

from unittest.mock import patch

def external_api_call():
    # Предположим, это обращение к внешнему API
    return "real response"

@patch("__main__.external_api_call")
def test_mocked_api(mock_api):
    mock_api.return_value = "mocked response"

    result = external_api_call()
    assert result == "mocked response"

Продвинутые техники

1. Автоспецификация

Создает mock с теми же атрибутами, что и оригинальный объект:

from unittest.mock import create_autospec

def my_function(x, y):
    return x + y

mock_func = create_autospec(my_function)
mock_func(1, 2)  # Ок
mock_func(1)     # Вызовет TypeError (не хватает аргументов)

2. Side effects

Могут возвращать разные значения или вызывать исключения:

mock = Mock()
mock.side_effect = [1, 2, ValueError("Boom!")]

assert mock() == 1
assert mock() == 2
with pytest.raises(ValueError, match="Boom!"):
    mock()

3. Патчинг классов

with patch("module.ClassName") as MockClass:
    instance = MockClass.return_value
    instance.method.return_value = "foo"

    obj = module.ClassName()
    assert obj.method() == "foo"

Пример из реальной практики

Тестирование сервиса, работающего с базой данных:

from unittest.mock import patch, MagicMock
import pytest

def get_user_from_db(user_id):
    # Реальный вызов к базе данных
    pass

@patch("module.get_user_from_db")
def test_user_service(mock_db):
    # Настраиваем mock для разных сценариев
    mock_db.return_value = {"id": 1, "name": "Test User"}

    user = get_user_from_db(1)
    assert user["name"] == "Test User"

    # Проверяем что функция была вызвана с правильным аргументом
    mock_db.assert_called_once_with(1)

Ограничения и подводные камни

  1. Слишком много моков делает тесты хрупкими
  2. Моки могут скрывать проблемы интеграции
  3. Не заменяют интеграционные тесты
  4. Могут усложнять понимание тестов

Альтернативы

  1. Fake objects - упрощенные реализации (например, in-memory база данных)
  2. Stubs - объекты с жестко заданным поведением
  3. Spies - обертки вокруг реальных объектов, записывающие вызовы

Резюмируем

Mock-объекты — мощный инструмент для unit-тестирования, позволяющий:

  • Изолировать тестируемый код от внешних зависимостей
  • Контролировать поведение зависимостей
  • Проверять взаимодействие между компонентами
  • Ускорять выполнение тестов

Главное правило: используйте моки разумно, не превращая тесты в проверку работы самих моков. Хороший тест проверяет логику кода, а не реализацию моков.