У unittest.mock.Mock в side effect передали последовательность значений. Что произойдёт при последующих выз...

У unittest.mock.Mock в side_effect передали последовательность значений. Что произойдёт при последующих вызовах?

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

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

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

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

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

Параметр side_effect решает эту задачу единым механизмом. Он позволяет задать функцию, исключение или итерируемую последовательность результатов.

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

Предположим, код вызывает одну и ту же зависимость несколько раз, а тест должен проверить реакцию на разные ответы. Если передать последовательность в side_effect, её элементы будут использованы не как независимый шаблон для каждого вызова, а как одноразовый поток значений.

Неверное понимание этого поведения приводит к неожиданному StopIteration, особенно если мок хранится дольше одного теста или вызывается больше раз, чем предусмотрено сценарием.

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

Если side_effect получает итерируемый объект, Mock создаёт или использует итератор и извлекает из него значения через next. Поэтому первый вызов получает первый элемент, второй — второй и так далее.

from unittest.mock import Mock client = Mock(side_effect=[{'status': 'retry'}, {'status': 'ok'}]) assert client() == {'status': 'retry'} assert client() == {'status': 'ok'} try: client() except StopIteration: pass

Список в примере превращается в последовательность, которая потребляется постепенно. При исчерпании элементов Mock не возвращает return_value, а пробрасывает StopIteration.

Это отличается от передачи функции в side_effect: в таком случае функция вызывается при каждом обращении к моку. Также side_effect может содержать экземпляр или класс исключения; тогда соответствующий вызов выбросит это исключение вместо возврата значения.

Состояние итератора не сбрасывается обычным вызовом reset_mock(): этот метод очищает историю вызовов, но не перематывает уже потреблённую последовательность. Для повторного сценария нужно назначить новый итерируемый объект или заново настроить side_effect.

Главный компромисс — точность сценария против устойчивости теста. Последовательность хорошо описывает конечный протокол взаимодействия, но делает тест зависимым от количества и порядка вызовов.

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

Сервис повторяет запрос к API после ответа с признаком временной ошибки. В тесте нужно проверить, что первый ответ приводит к повтору, а второй завершает операцию успешно.

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

Можно задать последовательность значений через side_effect. Такой вариант явно показывает протокол API и проще проверяется, поэтому он предпочтителен для короткого фиксированного сценария. Если число вызовов превысит ожидаемое, StopIteration быстро выявит ошибку в реализации или в тесте.

Если один и тот же мок используется в нескольких тестах, безопаснее создавать его заново для каждого теста либо явно назначать новый side_effect. Это предотвращает утечку состояния потреблённого итератора между сценариями.

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

  1. Что произойдёт, если одним из элементов side_effect будет исключение?

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

  2. Сбрасывает ли reset_mock() позицию в последовательности side_effect?

    Нет. reset_mock() очищает сведения о вызовах, например call_count и call_args, но не перематывает итератор, переданный в side_effect. Если последовательность уже частично или полностью потреблена, для повторения сценария нужно присвоить новый список, итератор или другой итерируемый объект.

  3. Как соотносятся side_effect и return_value, если они заданы одновременно?

    side_effect имеет приоритет. При итерируемом side_effect результат берётся из очередного элемента, поэтому обычный return_value не используется. Исключение составляет специальное значение DEFAULT: если оно встретилось в последовательности, Mock использует настроенный return_value для этого конкретного вызова.