← Назад к вопросам
В чём разница между 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
| Операция | Mock | MagicMock |
|---|---|---|
| Вызов функции | ✓ | ✓ |
| Присваивание атрибутов | ✓ | ✓ |
| Проверка вызовов | ✓ | ✓ |
| Арифметические операции | ✗ (ошибка) | ✓ |
| Сравнения (==, <, >) | ✗ | ✓ |
| Контекст-менеджеры (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_* для валидации поведения