← Назад к вопросам
Как настроить mock так, чтобы он возвращал разные значения при нескольких вызовах?
2.3 Middle🔥 151 комментариев
#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Mock с разными возвращаемыми значениями: Практический гайд
В тестировании часто нужно, чтобы одна функция возвращала разные значения при разных вызовах. Это критическое знание для качественного тестирования. Я использую несколько подходов в зависимости от ситуации.
Метод 1: side_effect с List
Простой и прямой способ — передать список значений в side_effect.
from unittest.mock import Mock
# Mock возвращает разные значения при каждом вызове
mock_api = Mock()
mock_api.get_user.side_effect = [
{'id': 1, 'name': 'John'},
{'id': 2, 'name': 'Jane'},
{'id': 3, 'name': 'Bob'},
]
# Первый вызов
result1 = mock_api.get_user(1)
print(result1) # {'id': 1, 'name': 'John'}
# Второй вызов
result2 = mock_api.get_user(2)
print(result2) # {'id': 2, 'name': 'Jane'}
# Третий вызов
result3 = mock_api.get_user(3)
print(result3) # {'id': 3, 'name': 'Bob'}
# Четвёртый вызов — исключение
try:
mock_api.get_user(4)
except StopIteration:
print("No more values in side_effect") # Выходит исключение
Метод 2: side_effect с Callable (функция)
Для более сложной логики используй функцию.
from unittest.mock import Mock
call_count = 0
def custom_side_effect(*args, **kwargs):
global call_count
call_count += 1
if call_count == 1:
return {'status': 'processing'}
elif call_count == 2:
return {'status': 'pending'}
else:
return {'status': 'completed'}
mock_job = Mock()
mock_job.get_status.side_effect = custom_side_effect
print(mock_job.get_status()) # {'status': 'processing'}
print(mock_job.get_status()) # {'status': 'pending'}
print(mock_job.get_status()) # {'status': 'completed'}
Метод 3: Использование @patch с side_effect
Для интеграционных тестов с реальным кодом.
from unittest.mock import patch
# Функция которую мы тестируем
def process_users(api):
results = []
for i in range(3):
user = api.get_user(i)
results.append(user)
return results
# Тест
with patch('module.api.get_user') as mock_get:
mock_get.side_effect = [
{'id': 0, 'name': 'Alice'},
{'id': 1, 'name': 'Bob'},
{'id': 2, 'name': 'Charlie'},
]
results = process_users(None)
assert len(results) == 3
assert results[0]['name'] == 'Alice'
Метод 4: side_effect с исключениями
Мимикрирование ошибок при разных вызовах.
from unittest.mock import Mock
mock_db = Mock()
mock_db.query.side_effect = [
[{'id': 1}], # Первый вызов успешный
Exception('Connection timeout'), # Второй вызов ошибка
[{'id': 2}], # Третий вызов успешный
]
# Первый вызов
result1 = mock_db.query()
print(result1) # [{'id': 1}]
# Второй вызов — исключение
try:
result2 = mock_db.query()
except Exception as e:
print(f"Error: {e}") # Connection timeout
# Третий вызов
result3 = mock_db.query()
print(result3) # [{'id': 2}]
Метод 5: side_effect с Iterator
Для более гибкого управления.
from unittest.mock import Mock
from itertools import cycle
# Циклический iterator (бесконечный)
mock_service = Mock()
mock_service.get_status.side_effect = cycle(['ready', 'busy', 'idle'])
for i in range(6):
print(mock_service.get_status()) # ready, busy, idle, ready, busy, idle
Метод 6: Использование wraps для частичного мокирования
Мокируем, но сохраняем частичную функциональность.
from unittest.mock import Mock
from unittest.mock import patch
class RealAPI:
def __init__(self):
self.call_count = 0
def get_data(self, user_id):
self.call_count += 1
if self.call_count == 1:
return {'data': 'first'}
return {'data': 'second'}
api = RealAPI()
mock_api = Mock(wraps=api)
result1 = mock_api.get_data(1)
result2 = mock_api.get_data(2)
print(result1) # {'data': 'first'}
print(result2) # {'data': 'second'}
print(mock_api.get_data.call_count) # 2
Практический пример: Тестирование retry логики
from unittest.mock import Mock, patch
import pytest
from requests.exceptions import ConnectionError
# Код который мы тестируем
def fetch_with_retry(api, url, max_retries=3):
for attempt in range(max_retries):
try:
return api.get(url)
except ConnectionError:
if attempt == max_retries - 1:
raise
continue
# Тест
def test_fetch_with_retry_succeeds_after_failures():
mock_api = Mock()
# Первые 2 вызова ошибка, 3-й успешный
mock_api.get.side_effect = [
ConnectionError('Failed'),
ConnectionError('Failed'),
{'status': 'success', 'data': 'hello'}
]
result = fetch_with_retry(mock_api, 'http://api.example.com/data')
assert result == {'status': 'success', 'data': 'hello'}
assert mock_api.get.call_count == 3 # Вызвали 3 раза
Сложный пример: Автоинкрементирующиеся ID
from unittest.mock import Mock
# Создание ID при каждом вызове
def id_generator():
counter = 0
while True:
counter += 1
yield {'id': counter, 'timestamp': f'2026-03-22T{counter}:00:00'}
mock_db = Mock()
mock_db.create_record.side_effect = id_generator()
# Каждый вызов вернёт новый уникальный ID
record1 = mock_db.create_record({'name': 'John'})
record2 = mock_db.create_record({'name': 'Jane'})
record3 = mock_db.create_record({'name': 'Bob'})
print(record1) # {'id': 1, 'timestamp': '2026-03-22T1:00:00'}
print(record2) # {'id': 2, 'timestamp': '2026-03-22T2:00:00'}
print(record3) # {'id': 3, 'timestamp': '2026-03-22T3:00:00'}
Продвинутый пример: side_effect зависит от аргументов
from unittest.mock import Mock
def smart_side_effect(user_id):
"""Логика зависит от аргументов"""
if user_id < 0:
raise ValueError("Invalid user ID")
elif user_id == 0:
return None # User not found
else:
return {'id': user_id, 'name': f'User{user_id}'}
mock_api = Mock()
mock_api.get_user.side_effect = smart_side_effect
# Тестируем разные сценарии
assert mock_api.get_user(1) == {'id': 1, 'name': 'User1'}
assert mock_api.get_user(0) is None
try:
mock_api.get_user(-1)
except ValueError:
print("Correctly raised ValueError")
Пример с pytest fixtures
import pytest
from unittest.mock import Mock
@pytest.fixture
def mock_database():
mock = Mock()
# Конфигурируем mock с разными значениями
mock.get_user.side_effect = [
{'id': 1, 'name': 'Alice'},
{'id': 2, 'name': 'Bob'},
None, # User not found
]
return mock
def test_get_users(mock_database):
assert mock_database.get_user(1) == {'id': 1, 'name': 'Alice'}
assert mock_database.get_user(2) == {'id': 2, 'name': 'Bob'}
assert mock_database.get_user(3) is None
Типичные ошибки
# ОШИБКА 1: Забыли про StopIteration
mock = Mock()
mock.method.side_effect = [1, 2] # Только 2 значения
assert mock.method() == 1
assert mock.method() == 2
# mock.method() # <- Вызовет StopIteration!
# ОШИБКА 2: side_effect не сбрасывается между тестами
# (Используй @pytest.fixture с scope='function')
# ОШИБКА 3: Забыли про reset_mock()
mock = Mock()
mock.method.side_effect = [1, 2]
mock.method() # first call
mock.reset_mock() # Сброс!
mock.method.side_effect = [3, 4] # Новые значения
mock.method() # Вернёт 3, не 2
Итоговая рекомендация
| Ситуация | Решение |
|---|---|
| Простой список значений | side_effect=[val1, val2, val3] |
| Сложная логика | side_effect=custom_function |
| Бесконечные значения | side_effect=cycle([...]) |
| Зависит от аргументов | side_effect=lambda x: compute(x) |
| Исключения | side_effect=[value, Exception(...)] |
| Генератор уникальных ID | side_effect=generator_function() |
Мой совет: Всегда используй side_effect вместо return_value, когда нужны разные значения при разных вызовах. Это делает тесты явными и предсказуемыми.