Программирование PythonТестированиеPython-разработчик, занимающийся модульным тестированием

Сравните поведение unittest.mock.Mock с параметром wraps и обычного Mock без него при вызове метода: откуда...

Сравните поведение unittest.mock.Mock с параметром wraps и обычного Mock без него при вызове метода: откуда берётся возвращаемое значение?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Mock с wraps делегирует вызов реальному объекту, поэтому возвращает результат настоящего метода, но одновременно записывает вызов для проверки. Обычный Mock реальный метод не вызывает и обычно возвращает другой объект Mock, если явно не задано иное.

Исторический контекст

Моки появились как тестовые двойники для изоляции тестируемого кода от внешних зависимостей и для проверки взаимодействий между объектами. В Python модуль unittest.mock объединил две полезные возможности: замену зависимости и регистрацию вызовов.

Параметр wraps предназначен для частичной подмены, когда настоящая реализация должна продолжить работать, но тесту нужно наблюдать за обращениями к ней. Такой объект часто называют spy, хотя Mock с wraps остаётся настраиваемым тестовым двойником.

Постановка проблемы

Полная замена объекта обычным Mock безопасно изолирует тест, но может скрыть ошибки в реальной реализации или изменить поведение, зависящее от состояния объекта. Простая проверка результата в этом случае не показывает, был ли действительно выполнен нужный метод.

Без wraps вызов не доходит до production-кода. Если разработчик ожидает реальный расчёт, но не задал return_value, тест получает объект-мок вместо настоящего результата и может либо упасть, либо пройти с неверными проверками.

Подробное решение

При создании Mock(wraps=реальный_объект) мок сохраняет ссылку на объект. При обращении к его вызываемому атрибуту создаётся дочерний мок, который также знает соответствующий реальный атрибут и делегирует ему вызов.

Результат реального метода возвращается вызывающему коду, а информация о вызове сохраняется в моках: аргументы доступны через call_args, количество вызовов — через call_count, а проверки выполняются методами вроде assert_called_once_with.

from unittest.mock import Mock class Calculator: def add(self, left, right): return left + right calculator = Mock(wraps=Calculator()) result = calculator.add(2, 3) assert result == 5 calculator.add.assert_called_once_with(2, 3)

Явно заданный return_value имеет приоритет над делегированием: реальный метод тогда не вызывается, а возвращается настроенное значение. Поэтому wraps не означает безусловное выполнение настоящего кода.

Вызов с wraps может изменять состояние реального объекта, обращаться к базе данных, файлам или сети. Это важное ограничение: наблюдение за вызовами не превращает такой тест в полностью изолированный unit-тест.

Ситуация из практики

Сервис расчёта тарифа использует объект правил, и тест должен убедиться, что правило вызвано с правильными аргументами, сохранив настоящий расчёт. Полная замена правила на обычный Mock позволяет легко задать результат, но проверяет только взаимодействие и не обнаруживает ошибку в самом расчёте.

Вызов реального объекта напрямую проверяет расчёт, однако не даёт удобной информации о том, с какими аргументами он был вызван. Mock(wraps=реальный_объект) совмещает оба свойства: сохраняет реальное поведение и предоставляет API проверки вызовов.

Выбранный вариант подходит, если расчёт детерминирован и не имеет нежелательных побочных эффектов. Если же реальный объект обращается к сети или изменяет важное состояние, лучше использовать обычный Mock с явно заданным результатом, а реальную реализацию проверять отдельными тестами.

Что кандидаты часто упускают

  1. Делегирует ли wraps вызовы методов, если у мока задано return_value?

    Нет. Явный return_value переопределяет делегирование для этого вызываемого мока. Вызов вернёт настроенное значение, а реальный метод не выполнится. Это позволяет точечно заменить один метод даже у объекта, остальные методы которого продолжают работать через wraps.

  2. Изолирует ли Mock(wraps=объект) побочные эффекты реального объекта?

    Нет. wraps только наблюдает за вызовом и делегирует его; он не копирует объект и не откатывает изменения его состояния. Если метод пишет в файл или вызывает внешний сервис, эти действия произойдут, поэтому для изоляции следует использовать обычный мок либо отдельную подмену конкретной зависимости.

  3. Будет ли зафиксирован вызов с wraps, если реальный метод завершится исключением?

    Да. Мок регистрирует факт вызова и аргументы до выполнения делегированного вызова. Затем исключение из реального метода обычно передаётся дальше, но проверка вызова через assert_called_once_with всё равно может подтвердить, что обращение произошло.