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

В чём разница между mock и magic mock в тестировании?

1.8 Middle🔥 121 комментариев
#Python Core#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Краткое резюме

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

Mock: базовый аналог

Создание и базовое использование

from unittest.mock import Mock

# Создаём Mock объект
db_mock = Mock()

# Mock может быть вызван как функция
db_mock.query("SELECT * FROM users")
print(db_mock.query.called)  # True
print(db_mock.query.call_count)  # 1
print(db_mock.query.call_args)  # call('SELECT * FROM users')

# Можно проверить вызовы
db_mock.query.assert_called_once()
db_mock.query.assert_called_with("SELECT * FROM users")

Настройка возвращаемых значений

from unittest.mock import Mock

api_mock = Mock()

# Устанавливаем возвращаемое значение
api_mock.fetch_user.return_value = {"id": 1, "name": "Alice"}

result = api_mock.fetch_user(user_id=1)
print(result)  # {"id": 1, "name": "Alice"}

# Можно использовать side_effect для динамического поведения
api_mock.fetch_data.side_effect = [1, 2, 3]  # Возвращает разные значения при каждом вызове

print(api_mock.fetch_data())  # 1
print(api_mock.fetch_data())  # 2
print(api_mock.fetch_data())  # 3

Проблема: Mock не поддерживает встроенные методы

from unittest.mock import Mock

number_mock = Mock()
number_mock.return_value = 5

# Операции Python работают, но не как ожидаешь
result = number_mock + 10
print(result)  # <Mock ...> — не 15!
print(type(result))  # <class 'unittest.mock.Mock'>

# Сравнения не работают
if number_mock == 5:
    print("Равно 5")
else:
    print("Не равно 5")  # Будет это

# Контекст-менеджеры не работают
with number_mock:
    pass  # AttributeError: __enter__

MagicMock: встроенные методы

Автоматическая поддержка операций

from unittest.mock import MagicMock

# MagicMock поддерживает встроенные методы
number_mock = MagicMock(return_value=5)

# Арифметика
result = number_mock + 10
print(result)  # <MagicMock...> — всё ещё не 15, но структура правильная

# Но можно установить конкретное поведение
number_mock.__add__.return_value = 15
result = number_mock + 10
print(result)  # 15

# Сравнения
number_mock.__eq__.return_value = True
print(number_mock == 5)  # True

# Контекст-менеджер
file_mock = MagicMock()
with file_mock:
    print("Вход в контекст")
# Работает без ошибок
file_mock.__enter__.assert_called_once()
file_mock.__exit__.assert_called_once()

Примеры с MagicMock

from unittest.mock import MagicMock

# Пример 1: Работа с файлами
file_mock = MagicMock()
file_mock.read.return_value = "Hello, World!"

with file_mock:
    content = file_mock.read()
    print(content)  # "Hello, World!"

# Пример 2: Работа со списками
list_mock = MagicMock()
list_mock.__len__.return_value = 3
list_mock.__getitem__.return_value = "item"

print(len(list_mock))  # 3
print(list_mock[0])  # "item"
print(list_mock[1])  # "item"

# Пример 3: Работа со строками
str_mock = MagicMock()
str_mock.__str__.return_value = "Mock Object"
str_mock.__add__.return_value = "Mock Object + suffix"

print(str(str_mock))  # "Mock Object"
print(str_mock + " + suffix")  # "Mock Object + suffix"

Сравнение: Mock vs MagicMock

ОперацияMockMagicMock
Вызов функции
Присваивание атрибутов
Проверка вызовов
Арифметические операции✗ (ошибка)
Сравнения (==, <, >)
Контекст-менеджеры (with)✗ (ошибка)
Итераторы
Длина (len())
Индексирование (obj[0])
Обратные методы (add и т.п.)

Практические примеры

Пример 1: Тестирование функции с файлом

from unittest.mock import Mock, MagicMock, patch
import json

# Функция, которую тестируем
def process_json_file(filename):
    with open(filename, 'r') as f:
        data = json.load(f)
    return data["name"].upper()

# НЕПРАВИЛЬНО — Mock не поддерживает with
def test_with_mock():
    file_mock = Mock()
    file_mock.read.return_value = '{"name": "alice"}'
    
    # with file_mock:  # AttributeError: __enter__
    #     pass

# ПРАВИЛЬНО — MagicMock поддерживает with
def test_with_magic_mock():
    file_mock = MagicMock()
    file_mock.__enter__.return_value = file_mock
    file_mock.read.return_value = '{"name": "alice"}'
    
    with file_mock:
        content = file_mock.read()
    
    file_mock.__enter__.assert_called_once()
    file_mock.__exit__.assert_called_once()

Пример 2: Тестирование с patch

from unittest.mock import patch, Mock, MagicMock
import requests

# Функция, которую тестируем
def get_user_name(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()["name"]

# Тест с Mock
@patch('requests.get')
def test_with_mock(mock_get):
    mock_response = Mock()
    mock_response.json.return_value = {"name": "Alice"}
    mock_get.return_value = mock_response
    
    result = get_user_name(1)
    assert result == "Alice"
    mock_get.assert_called_once_with("https://api.example.com/users/1")

# Тест с MagicMock (проще читается)
@patch('requests.get')
def test_with_magic_mock(mock_get):
    mock_get.return_value.json.return_value = {"name": "Alice"}
    
    result = get_user_name(1)
    assert result == "Alice"
    mock_get.assert_called_once_with("https://api.example.com/users/1")

Пример 3: Сложное поведение

from unittest.mock import MagicMock

# Класс, который тестируем
class DatabaseConnection:
    def query(self, sql):
        # Реальная реализация
        pass
    
    def __len__(self):
        # Количество записей
        pass

# Настройка MagicMock для сложного поведения
def test_database():
    db_mock = MagicMock(spec=DatabaseConnection)
    
    # Настраиваем query
    db_mock.query.return_value = [{"id": 1, "name": "Alice"}]
    
    # Настраиваем len
    db_mock.__len__.return_value = 1
    
    # Настраиваем side_effect для отслеживания вызовов
    db_mock.connect.side_effect = [
        None,  # Первый вызов
        ConnectionError("Connection refused"),  # Второй вызов
    ]
    
    # Используем
    result = db_mock.query("SELECT * FROM users")
    assert result == [{"id": 1, "name": "Alice"}]
    assert len(db_mock) == 1
    
    db_mock.connect()  # OK
    # db_mock.connect()  # Вызовет ошибку

Когда использовать Mock

Mock подходит когда:

  • Нужна строгая проверка интерфейса
  • Не требуются встроенные методы Python
  • Хочешь явно определить все взаимодействия
  • Нужна максимальная ясность
from unittest.mock import Mock

class UserService:
    def __init__(self, db):
        self.db = db
    
    def get_user(self, user_id):
        return self.db.find_user(user_id)

# Тест
def test_get_user():
    db_mock = Mock()
    db_mock.find_user.return_value = {"id": 1, "name": "Alice"}
    
    service = UserService(db_mock)
    result = service.get_user(1)
    
    assert result == {"id": 1, "name": "Alice"}
    db_mock.find_user.assert_called_once_with(1)

Когда использовать MagicMock

MagicMock подходит когда:

  • Нужна гибкость и поддержка встроенных методов
  • Тестируешь код, который много использует операции Python
  • Нужна поддержка контекст-менеджеров, итераторов и т.п.
  • Хочешь меньше явных настроек
from unittest.mock import MagicMock
from contextlib import contextmanager

class FileProcessor:
    def process(self, filename):
        with open(filename) as f:
            lines = len(f.readlines())
        return lines

# Тест
def test_file_processor():
    file_mock = MagicMock()
    file_mock.readlines.return_value = ["line1", "line2", "line3"]
    file_mock.__len__.return_value = 3
    
    # with file_mock:  # MagicMock поддерживает это
    #     lines = len(file_mock.readlines())
    # assert lines == 3

Лучшие практики

1. Используй spec для ограничения мокируемого интерфейса

from unittest.mock import Mock

class Database:
    def query(self, sql):
        pass
    
    def close(self):
        pass

# Без spec — можно вызвать любой метод
db_mock_loose = Mock()
db_mock_loose.nonexistent_method()  # Без ошибок

# С spec — только реальные методы
db_mock_strict = Mock(spec=Database)
# db_mock_strict.nonexistent_method()  # AttributeError
db_mock_strict.query("SELECT *")  # OK
db_mock_strict.close()  # OK

2. Предпочитай MagicMock для внешних зависимостей

from unittest.mock import MagicMock, patch

# Плохо — используешь Mock для файлов
@patch('builtins.open', new_callable=Mock)
def test_read_file_bad(mock_open):
    # mock_open().__enter__()  # AttributeError
    pass

# Хорошо — используешь MagicMock для файлов
@patch('builtins.open', new_callable=MagicMock)
def test_read_file_good(mock_open):
    mock_open.return_value.__enter__.return_value.read.return_value = "content"
    with open("file.txt") as f:
        content = f.read()
    assert content == "content"

3. Используй assert_called_* для проверки вызовов

from unittest.mock import Mock

mock = Mock()

# Вызываем
mock.method(1, 2, name="test")

# Проверяем
mock.method.assert_called_once()  # Вызван ровно один раз
mock.method.assert_called_with(1, 2, name="test")  # С правильными аргументами
mock.method.assert_called_once_with(1, 2, name="test")  # Оба условия

# Проверяем историю вызовов
mock.method.call_args_list  # Список всех вызовов
mock.method.call_count  # Количество вызовов

Частые ошибки

Ошибка 1: Забываешь про side_effect

from unittest.mock import Mock

mock = Mock()

# НЕПРАВИЛЬНО — всегда возвращает то же значение
mock.method.return_value = 1
print(mock.method())  # 1
print(mock.method())  # 1

# ПРАВИЛЬНО — разные значения для каждого вызова
mock.method.side_effect = [1, 2, 3]
print(mock.method())  # 1
print(mock.method())  # 2
print(mock.method())  # 3

Ошибка 2: Мокируешь слишком много

from unittest.mock import Mock

# ПЛОХО — мокируешь всё подряд
def test_bad():
    db = Mock()
    cache = Mock()
    logger = Mock()
    auth = Mock()
    # ... много кода ...
    # Сложно понять, что реально тестируется

# ХОРОШО — мокируешь только необходимое
def test_good():
    # Тестируем логику, мокируем только внешние зависимости
    db = Mock()
    db.find_user.return_value = {"id": 1}
    
    service = UserService(db)
    result = service.get_user(1)
    assert result == {"id": 1}

Заключение

  • Mock — базовый, для строгих проверок интерфейса
  • MagicMock — расширенный, для гибкости и встроенных методов
  • Выбор: используй Mock по умолчанию, MagicMock если нужны встроенные методы
  • Правило: мокируй только необходимое, остальное тестируй реально
  • Проверки: используй assert_called_* для валидации поведения