Мокирование возвращаемых значений функции в Python: unit-тесты
Быстрый ответ
Если требуется принудить объект мока возвращать значения по порядку, следует использовать атрибут side_effect
библиотеки unittest.mock
. Вам потребуется установить этот атрибут следующим образом:
from unittest.mock import MagicMock
mock_func = MagicMock()
# Последовательный возврат значениями: 'первый', 'второй', 'третий'
mock_func.side_effect = ['первый', 'второй', 'третий']
# Ваш последовательный вызов функции-мока
print(mock_func()) # Выведет: 'первый'
print(mock_func()) # Выведет: 'второй'
print(mock_func()) # Выведет: 'третий'
Для этого присваивается side_effect
итерируемый объект с требуемыми значениями. Каждый вызов mock_func
вернёт следующее по порядку значение из перечисленных.
Если вам необходима более сложная логика ответа, используйте функцию или итератор в качестве side_effect
. Это даст большую гибкость и позволит сценарию динамически имитировать ответы.
Использование итераторов и итерируемых объектов в качестве side_effect
Создание mock-объекта с итерируемым объектом в side_effect
Вы можете напрямую присвоить список или любой иной итерируемый объект в side_effect
:
mock_obj = Mock()
mock_obj.some_method.side_effect = ["Я Бэтмен!", "Нет, я Бэтмен!", "Нет, БЭТМЕН – это я..."]
Теперь mock_obj.some_method()
при каждом вызове будет возвращать новую фразу о Бэтмене.
Более точное управление возвращаемыми значениями с помощью итератора
Для более точного контроля возвращаемыми значениями можно использовать сочетание функций iter()
и patch()
, присваивая side_effect
:
return_values = iter(['да', 'нет', 'может быть'])
with patch('module.func', side_effect=return_values) as mocked_func:
# Здесь вы можете выполнять свои действия
...
Состояние итератора сохраняется между вызовами и результаты возвращаются последовательно.
Тестирование функций с переменными возвращаемыми значениями
Управление вызовами функций без учета входных данных
Если у функции нет входных данных, можно установить разные возвращаемые значения:
mock_obj = Mock(return_value=None)
mock_obj.method.side_effect = ['клубничный', 'черничный', 'яблочный']
Проверка соответствия вызовов и результатов
После того как вы настроили side_effect
, можно создать тесты для проверки:
- Количество вызовов функции соответствует ожидаемому.
- Последовательность возвращаемых значений соответствует массиву
side_effect
.
Использование исключений в side_effect
Если требуется последовательно генерировать исключения в сочетании с подходящими возвращаемыми значениями:
mock_obj = Mock()
mock_obj.broken_method.side_effect = ['утка', 'утка', GooseError('ГУСЬ!')]
Запомните, что при срабатывании исключения оставшиеся вызовы не выполняются, если исключение не обработано в коде теста.
Настройка параметров поведения функций
Разные входные данные соответствуют разным выходным
Если разные входные данные должны вызывать конкретные ответы:
mock_obj = Mock()
def side_effect(arg):
returns = {'яблоко': 1, 'банан': 2, 'киви': 3}
return returns.get(arg, 'Сюрприз!')
mock_obj.method.side_effect = side_effect
Это позволит возвращать разные результаты для разных входных данных.
Динамически изменяющиеся возвращаемые значения
Если необходимо имитировать динамично меняющееся поведение:
return_values = ['подъем', 'круиз', 'спуск', 'посадка']
def dynamic_side_effect(*args):
return return_values.pop(0)
mock_obj.method.side_effect = dynamic_side_effect
Таким образом, можно имитировать процесс, который меняется со временем.
Визуализация
Представьте мок-функцию как диджея, который по требованию включает различные треки:
from unittest.mock import Mock
# DJ с ротационным плейлистом
dj = Mock()
dj.play.side_effect = ['🎶 Трек 1', '🎶 Трек 2', '🎶 Трек 3']
При каждом запросе пользователя будет включаться следующий трек:
📻 ➡️ 🎶 Трек 1
Следующий запрос:
📻 ➡️ 🎶 Трек 2
И следующий:
📻 ➡️ 🎶 Трек 3
Так же как и mock
, который возвращает заданные заранее значения.
Управление множеством mock-объектов с помощью side_effect
Подмена методов экземпляра
При подмене методов экземпляра убедитесь, что цель подмены выбрана корректно:
with patch.object(SomeClass, 'method', side_effect=[...]) as mock_method:
instance = SomeClass()
instance.method() # Выполняет первый вызов
...
Подмена классовых и статических методов
Классовые и статические методы подменяются с помощью mock:
with patch.object(SomeClass, 'class_method', side_effect=[...]) as mock_class_method:
...
with patch.object(SomeClass, 'static_method', side_effect=[...]) as mock_static_method:
...
Подмена свойств с использованием side_effect
Для отображения изменений свойств используйте side_effect
в сочетании с PropertyMock
:
with patch('module.Class.property', new_callable=PropertyMock) as mock_property:
mock_property.side_effect = [val1, val2, val3]
...
Полезные материалы
- unittest.mock — mock-объекты в Python — Документация Python — Официальное руководство по использованию библиотеки unittest.mock.
- Python mock multiple return values – Stack Overflow — Обсуждения на Stack Overflow о множественных возвращаемых значениях при тестировании Python.
- Использование библиотеки Python Mock Object – Real Python — Статья о работе с объектами мок в Python.
- Мокирование в Python: как улучшить юнит-тесты | Toptal — Материал, посвященный использованию мокирования в Python.
- Разгадка функции Patch – PyCon 2018 – YouTube — Презентация на PyCon, разъясняющая основы работы с функцией patch.
- Python Test | 123: GitHub Actions – Tania Allard — Подкаст с полезными советами о использовании Pytest совместно с GitHub Actions.