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

После нескольких сценариев один и тот же Mock переиспользуют в тесте: что именно очищает reset mock, а что ...

После нескольких сценариев один и тот же Mock переиспользуют в тесте: что именно очищает reset_mock, а что сохраняет?

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

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

reset_mock() очищает историю вызовов мока и его дочерних моков, но по умолчанию сохраняет настроенные return_value, side_effect, спецификацию и произвольные атрибуты. Чтобы дополнительно сбросить возвращаемое значение или побочный эффект, их нужно явно запросить параметрами return_value=True и side_effect=True.

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

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

reset_mock появился как способ переиспользовать настроенную подмену без её полного пересоздания. Однако это именно сброс наблюдаемого состояния, а не возврат объекта к исходному состоянию без каких-либо настроек.

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

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

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

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

Без аргументов reset_mock() сбрасывает called, call_count, call_args, call_args_list, method_calls и mock_calls. Состояние дочерних моков, созданных при обращении к атрибутам или методам, также сбрасывается рекурсивно.

По умолчанию настройки поведения сохраняются. К ним относятся return_value, side_effect, spec, spec_set, wraps и созданные атрибуты. Поэтому после сброса мок остаётся той же подменой, но начинает с чистой историей взаимодействий.

Параметр return_value=True дополнительно сбрасывает возвращаемое значение, а side_effect=True удаляет побочный эффект. Эти параметры следует применять осознанно: если мок специально настроен для всех сценариев, лишний сброс поведения может сделать последующие проверки некорректными.

from unittest.mock import Mock client = Mock(return_value="cached") client("a") client.reset_mock() assert client.call_count == 0 assert client() == "cached" assert client.call_count == 1 client.reset_mock(return_value=True) assert isinstance(client(), Mock)

В примере первый сброс удаляет только историю вызовов, поэтому возвращаемое значение сохраняется. Второй сброс удаляет и настроенное return_value; новый результат создаётся уже самим моком.

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

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

Тест проверяет повторную отправку сообщения. В первой фазе мок клиента вызывается при первоначальной отправке, затем тест очищает историю и запускает повторную отправку. Вызвать reset_mock() здесь разумно: конфигурация клиента должна остаться прежней, а проверять нужно только взаимодействия второй фазы.

Можно создать новый мок для второй фазы. Это лучше изолирует состояния, но требует повторить настройку и может усложнить тест, если у мока много связанных дочерних объектов. Можно использовать reset_mock(return_value=True, side_effect=True), однако это удалит поведение, нужное второй фазе.

Оптимальное решение — обычный reset_mock(), если меняется только граница наблюдения, и явная перенастройка после сброса, если должен измениться сценарий поведения. Такой тест сохраняет независимость проверок и не скрывает, какие настройки действительно общие.

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

  1. Сбрасывает ли reset_mock() настройки дочерних моков?

Да, история вызовов дочерних моков сбрасывается рекурсивно. Например, обращения к service.client.send будут удалены из истории соответствующего дочернего объекта. Но его return_value, side_effect и прочие настройки по умолчанию сохраняются.

  1. Чем reset_mock() отличается от создания нового Mock?

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

  1. Можно ли считать тест изолированным, если перед ним вызван reset_mock()?

Не всегда. История вызовов будет очищена, но произвольные изменения атрибутов, настройки поведения и внешние эффекты самой тестируемой системы могут сохраниться. Для независимых тестов предпочтительна новая фикстура с областью действия на функцию; reset_mock() подходит для отдельных фаз внутри одного контролируемого сценария.