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

Как настроить 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(...)]
Генератор уникальных IDside_effect=generator_function()

Мой совет: Всегда используй side_effect вместо return_value, когда нужны разные значения при разных вызовах. Это делает тесты явными и предсказуемыми.

Как настроить mock так, чтобы он возвращал разные значения при нескольких вызовах? | PrepBro