← Назад к вопросам
Что такое mock и когда его использовать?
2.3 Middle🔥 111 комментариев
#DevOps и инфраструктура#Django
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Mock: что это и когда использовать
Что такое Mock?
Mock — это объект-подделка, который заменяет реальный объект во время тестирования. Mock позволяет:
- Изолировать тестируемый код
- Избежать вызовов внешних сервисов (API, БД)
- Контролировать поведение зависимостей
- Проверить, как код взаимодействует с зависимостями
1. Основное использование Mock
from unittest.mock import Mock, patch, MagicMock
import requests
# Наш код, который тестируем
def fetch_user(user_id: int) -> dict:
"""Получить пользователя по ID из API"""
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
# ТЕСТ БЕЗ MOCK (плохо)
def test_fetch_user_bad():
# Проблемы:
# 1. Делаем реальный HTTP запрос
# 2. Зависит от интернета
# 3. API может быть недоступна
# 4. Тест медленный (~1-5 секунд)
user = fetch_user(1)
assert user['id'] == 1
# ТЕСТ С MOCK (хорошо)
def test_fetch_user_with_mock():
# Создаем mock для requests.get
with patch('requests.get') as mock_get:
# Настраиваем mock
mock_response = Mock()
mock_response.json.return_value = {'id': 1, 'name': 'Alice'}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
# Вызываем функцию
user = fetch_user(1)
# Проверяем результат
assert user == {'id': 1, 'name': 'Alice'}
# Проверяем, что requests.get был вызван
mock_get.assert_called_once_with('https://api.example.com/users/1')
2. Основные объекты Mock
from unittest.mock import Mock, MagicMock, patch
# Mock
mock_obj = Mock()
mock_obj.method.return_value = "result"
print(mock_obj.method()) # "result"
# Проверяем вызовы
print(mock_obj.method.call_count) # 1
print(mock_obj.method.call_args) # call()
print(mock_obj.method.called) # True
# MagicMock (поддерживает magic методы)
magic = MagicMock()
magic[0] = "value" # __setitem__
print(magic[0]) # "value"
print(len(magic)) # __len__ работает
# patch (заменяет объект в контексте)
with patch('module.function') as mock_func:
mock_func.return_value = 42
# Во время этого блока module.function заменена на mock
3. Практический пример: тестирование с БД
from unittest.mock import Mock, patch
from typing import Optional
# Код который тестируем
class UserRepository:
def __init__(self, db_connection):
self.db = db_connection
def get_user(self, user_id: int) -> Optional[dict]:
user = self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
return user
def create_user(self, name: str, email: str) -> int:
self.db.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
(name, email)
)
self.db.commit()
return self.db.lastrowid()
# ТЕСТ
def test_get_user():
# Создаем mock БД
mock_db = Mock()
mock_db.query.return_value = {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
# Создаем репозиторий с mock БД
repo = UserRepository(mock_db)
# Вызываем метод
user = repo.get_user(1)
# Проверяем результат
assert user['name'] == 'Alice'
mock_db.query.assert_called_once()
def test_create_user():
mock_db = Mock()
mock_db.lastrowid.return_value = 42
repo = UserRepository(mock_db)
user_id = repo.create_user("Bob", "bob@example.com")
# Проверяем, что метод был вызван с правильными параметрами
assert user_id == 42
mock_db.execute.assert_called_once_with(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Bob", "bob@example.com")
)
mock_db.commit.assert_called_once()
4. Проверка вызовов Mock
from unittest.mock import Mock, call
mock_obj = Mock()
# Несколько вызовов
mock_obj.method(1)
mock_obj.method(2)
mock_obj.method(3)
# Проверки
print(mock_obj.method.call_count) # 3
# assert_called_once() — вызван ровно один раз
mock2 = Mock()
mock2.func()
mock2.func.assert_called_once()
# assert_called_with() — вызван с конкретными аргументами
mock3 = Mock()
mock3.func(1, 2, 3)
mock3.func.assert_called_with(1, 2, 3)
# assert_has_calls() — проверить последовательность вызовов
mock4 = Mock()
mock4.func(1)
mock4.func(2)
mock4.func.assert_has_calls([call(1), call(2)])
# assert_not_called() — не вызван вообще
mock5 = Mock()
mock5.func.assert_not_called()
5. Side Effects (сложное поведение)
from unittest.mock import Mock
# Side effect может быть:
# 1. Список значений (возвращаемые по порядку)
mock = Mock()
mock.side_effect = [1, 2, 3]
print(mock()) # 1
print(mock()) # 2
print(mock()) # 3
# print(mock()) # StopIteration
# 2. Функция (вызывается вместо mock)
mock2 = Mock()
mock2.side_effect = lambda x: x * 2
print(mock2(5)) # 10
print(mock2(3)) # 6
# 3. Исключение (выбросить при вызове)
mock3 = Mock()
mock3.side_effect = ValueError("Error!")
try:
mock3()
except ValueError as e:
print(e) # Error!
# 4. Комбинация (значения + исключение)
mock4 = Mock()
mock4.side_effect = [1, 2, ValueError("Done")]
print(mock4()) # 1
print(mock4()) # 2
try:
mock4()
except ValueError:
print("Exception raised")
6. patch() декоратор
from unittest.mock import patch
import requests
# Способ 1: Context manager
def test_with_context():
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {'status': 'ok'}
# requests.get заменена на mock внутри блока
response = requests.get('http://example.com')
assert response.json() == {'status': 'ok'}
# Способ 2: Декоратор функции
@patch('requests.get')
def test_with_decorator(mock_get):
mock_get.return_value.json.return_value = {'status': 'ok'}
response = requests.get('http://example.com')
assert response.json() == {'status': 'ok'}
# Способ 3: Несколько patch'ей
@patch('requests.post')
@patch('requests.get')
def test_multiple(mock_get, mock_post):
mock_get.return_value.json.return_value = {'id': 1}
mock_post.return_value.json.return_value = {'success': True}
resp1 = requests.get('http://example.com')
resp2 = requests.post('http://example.com', json={})
7. Тестирование обработки ошибок
from unittest.mock import Mock, patch
import requests
def fetch_data(url: str) -> dict:
try:
response = requests.get(url)
response.raise_for_status()
return response.json()
except requests.ConnectionError:
return {'error': 'Connection failed'}
except requests.HTTPError:
return {'error': 'HTTP error'}
def test_connection_error():
with patch('requests.get') as mock_get:
# Настраиваем mock выбросить исключение
mock_get.side_effect = requests.ConnectionError("Network error")
result = fetch_data('http://example.com')
assert result == {'error': 'Connection failed'}
def test_http_error():
with patch('requests.get') as mock_get:
mock_response = Mock()
mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
mock_get.return_value = mock_response
result = fetch_data('http://example.com')
assert result == {'error': 'HTTP error'}
Когда использовать Mock
| Ситуация | Используй Mock |
|---|---|
| Внешний API | ✅ Да (медленно, может быть недоступна) |
| База данных | ✅ Да (медленно, нужна test БД) |
| Файловая система | ✅ Да (медленно, оставляет файлы) |
| Текущее время | ✅ Да (недетерминировано) |
| Случайные числа | ✅ Да (непредсказуемо) |
| Простые функции | ❌ Нет (тестируй реально) |
| Внутренняя логика | ❌ Нет (интеграционные тесты) |
| Сетевые запросы | ✅ Да (VCR.py альтернатива) |
Лучшие практики
# ❌ ПЛОХО: Мокируешь слишком много
with patch('module.func1'), patch('module.func2'), patch('module.func3'):
# Сложно понять, что тестируешь
pass
# ✅ ХОРОШО: Мокируешь только внешние зависимости
with patch('external_api.get_user') as mock_api:
result = my_function() # Мокируется только API
assert result
# ✅ ХОРОШО: Используй assert_called_with для проверки
mock.func.assert_called_with(expected_arg)
# ✅ ХОРОШО: Очищай mock после теста (обычно pytest делает автоматически)