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

В чём состоит поведение не настроенного атрибута Mock при повторном обращении к нему?

В чём состоит поведение не настроенного атрибута Mock при повторном обращении к нему?

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

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

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

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

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

Моки появились как средство изолировать тестируемый код от внешних зависимостей: сети, файловой системы, базы данных или сложных сервисов. Автоматическое создание дочерних mock-объектов упростило моделирование объектов с вложенными интерфейсами без ручного описания всей структуры.

В unittest.mock такая гибкость сочетается с возможностью ограничить интерфейс с помощью спецификаций. Поэтому разработчик может выбрать между быстрым динамическим mock и более строгой проверкой контракта зависимости.

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

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

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

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

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

from unittest.mock import Mock service = Mock() first = service.client second = service.client assert first is second assert service.client.fetch() is not None service.client.fetch.return_value = 'ok' assert service.client.fetch() == 'ok'

В примере service.client — один и тот же дочерний mock. До настройки fetch его вызов возвращал бы ещё один автоматически созданный mock, а после настройки возвращает строку ok.

Автоматическая генерация не означает, что атрибут был вызван: само чтение service.client — это обращение к атрибуту, а не вызов метода. Проверять нужно именно нужное действие, например service.client.fetch.assert_called_once_with(...).

Для защиты от опечаток применяют spec или spec_set. Они ограничивают допустимые имена на основе заданного объекта или класса; при этом spec_set дополнительно запрещает присваивать атрибуты, отсутствующие в спецификации. Компромисс состоит в том, что строгие mock-объекты требуют поддерживать актуальную спецификацию зависимости, зато делают тесты надёжнее.

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

Код обработчика обращается к gateway.client.fetch_user(). В тесте разработчик случайно настроил gateway.clent.fetch_user.return_value. Обычный динамический Mock примет такую опечатку, а вызов gateway.client.fetch_user() останется ненастроенным и вернёт дочерний mock; проверка, не анализирующая результат строго, может пройти.

Можно оставить обычный mock: это быстро и удобно для прототипа, но опечатки не обнаруживаются. Можно вручную создавать и настраивать каждый вложенный mock: это прозрачно, однако тест становится многословным и хрупким. Предпочтительное решение — создать mock со спецификацией реального gateway и явно настроить ожидаемый путь client.fetch_user.

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

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

  1. Меняется ли дочерний mock при каждом обращении к атрибуту?

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

  1. Фиксирует ли обычный Mock факт чтения атрибута как вызов метода?

Нет. Получение service.client само по себе не является вызовом. В историю попадёт вызов дочернего mock, например service.client.fetch(), а не простое чтение client. Это важно, поскольку проверка вызовов должна соответствовать реальному требованию теста.

  1. Устраняет ли spec все риски динамических дочерних mock-объектов?

Нет. spec ограничивает доступ к атрибутам, описанным в спецификации, и помогает выявлять неизвестные имена. Однако он не проверяет автоматически всю семантику взаимодействия и не заменяет проверку аргументов, порядка вызовов или корректного результата. Для более строгого контроля используют подходящую спецификацию, autospec и явные проверки поведения.