Как заставить mock-объект в Python возвращать разные значения при последовательных вызовах

Ответ

В стандартной библиотеке unittest.mock для этой цели используется атрибут side_effect. Он позволяет гибко управлять поведением mock-объекта при каждом вызове.

1. Использование итерируемого объекта (например, списка)

Если передать в side_effect список, кортеж или любой другой итератор, при каждом вызове mock-объекта будет возвращаться следующий элемент этого итератора.

from unittest.mock import Mock

mock_obj = Mock()
mock_obj.side_effect = [10, 'hello', True, ValueError('An error')]

# Последовательные вызовы
print(mock_obj())  # 10
print(mock_obj())  # 'hello'
print(mock_obj())  # True

# Четвертый вызов вызовет исключение, указанное в списке
try:
    mock_obj()
except ValueError as e:
    print(e) # An error

# Пятый вызов вызовет StopIteration, так как элементы закончились
try:
    mock_obj()
except StopIteration:
    print('Iterator is exhausted')

2. Использование функции (callable)

Передача функции в side_effect позволяет реализовать динамическую логику. Эта функция будет вызываться с теми же аргументами, что и mock-объект.

from unittest.mock import Mock

def dynamic_return_value(*args, **kwargs):
    # Возвращаем значение в зависимости от переданных аргументов
    if 'id' in kwargs and kwargs['id'] == 1:
        return {'status': 'success'}
    return {'status': 'not_found'}

api_mock = Mock()
api_mock.side_effect = dynamic_return_value

print(api_mock(id=1))  # {'status': 'success'}
print(api_mock(id=2))  # {'status': 'not_found'}
print(api_mock())      # {'status': 'not_found'}

Краткий итог:

  • Список: для предопределенной последовательности возвращаемых значений или исключений.
  • Функция: для сложной логики, когда возвращаемое значение зависит от аргументов вызова.

Ответ 18+ 🔞

А, ну это же про моки, блядь! Слушай, тут есть одна офигенная штука — side_effect. Это как будто ты можешь запрограммировать, что твой муляж будет делать при каждом вызове. Не просто одно значение тупо возвращать, а целый спектакль разыгрывать, ёпта!

1. Засунуть туда список, как патроны в обойму

Кидаешь в side_effect список, и при каждом вызове он будет выплёвывать следующий элемент. Закончились элементы — всё, пиздец, StopIteration. Можно даже исключения подкинуть, чтоб проверить, как твой код от них отпиздуется.

from unittest.mock import Mock

mock_obj = Mock()
# Записываем программу: сначала число, потом строка, потом правда, а потом — сюрприз, блядь!
mock_obj.side_effect = [10, 'hello', True, ValueError('An error')]

# Пошли выстрелы по порядку
print(mock_obj())  # 10
print(mock_obj())  # 'hello'
print(mock_obj())  # True

# А вот тут уже подстава, ловим исключение
try:
    mock_obj()
except ValueError as e:
    print(e) # An error

# А дальше — пусто, патроны кончились, ебать!
try:
    mock_obj()
except StopIteration:
    print('Iterator is exhausted')

2. А можно свою функцию впихнуть — полный контроль!

Если списка мало, можно запихнуть туда свою функцию. Она будет вызываться каждый раз с теми же аргументами, что и мок. И ты внутри можешь творить любую хуйню в зависимости от того, что тебе передали.

from unittest.mock import Mock

def dynamic_return_value(*args, **kwargs):
    # Смотри, если в аргументах есть 'id' и он равен 1 — даём успех
    if 'id' in kwargs and kwargs['id'] == 1:
        return {'status': 'success'}
    # А иначе — пошёл нахуй, не найдено
    return {'status': 'not_found'}

api_mock = Mock()
api_mock.side_effect = dynamic_return_value

print(api_mock(id=1))  # {'status': 'success'}
print(api_mock(id=2))  # {'status': 'not_found'}
print(api_mock())      # {'status': 'not_found'}

Короче, суть в чём, блядь:

  • Список — это когда тебе надо проиграть заранее записанную плёнку с ответами. Раз, два, три, исключение — готово.
  • Функция — это когда тебе надо, чтобы твой муляж думал головой (ну, или делал вид, что думает) и реагировал на то, что ему скормили. Полная власть, ебать!