В чём состоит поведение не настроенного атрибута Mock при повторном обращении к нему?
При первом обращении к не настроенному атрибуту Mock создаётся дочерний mock-объект. При последующих обращениях к тому же атрибуту возвращается тот же объект, поэтому его состояние и история вызовов сохраняются.
Это позволяет моделировать цепочки обращений, но создаёт риск: опечатка в имени атрибута может незаметно породить новый mock вместо ошибки.
Моки появились как средство изолировать тестируемый код от внешних зависимостей: сети, файловой системы, базы данных или сложных сервисов. Автоматическое создание дочерних mock-объектов упростило моделирование объектов с вложенными интерфейсами без ручного описания всей структуры.
В unittest.mock такая гибкость сочетается с возможностью ограничить интерфейс с помощью спецификаций. Поэтому разработчик может выбрать между быстрым динамическим mock и более строгой проверкой контракта зависимости.
Если атрибут mock не настроен явно, обращение к нему обычно не завершается ошибкой. Вместо этого библиотека создаёт дочерний mock, который может участвовать в дальнейших вызовах и проверках.
Это удобно для сложных цепочек, но опасно при опечатках. Тест может пройти, хотя production-код обращается не к тому атрибуту или не вызывает реальный ожидаемый метод.
Дочерний объект создаётся лениво — только при первом обращении к атрибуту. Затем Mock кэширует его, поэтому проверка идентичности двух обращений будет истинной. Вызов дочернего объекта также записывается в историю вызовов родительского mock с учётом имени атрибута.
В примере 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.
В результате неверное имя атрибута обнаруживается при подготовке теста, а сам тест проверяет контракт зависимости, а не случайное поведение автоматически созданной цепочки.
Нет. Дочерний mock создаётся при первом обращении и сохраняется в структуре родительского объекта. Поэтому настройки, например return_value или side_effect, остаются доступными при следующих обращениях.
Нет. Получение service.client само по себе не является вызовом. В историю попадёт вызов дочернего mock, например service.client.fetch(), а не простое чтение client. Это важно, поскольку проверка вызовов должна соответствовать реальному требованию теста.
Нет. spec ограничивает доступ к атрибутам, описанным в спецификации, и помогает выявлять неизвестные имена. Однако он не проверяет автоматически всю семантику взаимодействия и не заменяет проверку аргументов, порядка вызовов или корректного результата. Для более строгого контроля используют подходящую спецификацию, autospec и явные проверки поведения.