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

При подмене класса через unittest.mock.patch что получит код при создании экземпляра внутри тестируемой фун...

При подмене класса через unittest.mock.patch что получит код при создании экземпляра внутри тестируемой функции?

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

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

Код получит не настоящий экземпляр класса, а значение return_value мока, созданного для подменённого класса. По умолчанию это дочерний mock, поэтому вызовы методов такого «экземпляра» можно отдельно проверять и настраивать.

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

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

Такой подход особенно полезен, когда создание объекта само по себе имеет побочные эффекты или требует недоступной инфраструктуры. Цена изоляции — риск проверить взаимодействие с mock-объектом, а не совместимость с реальным классом.

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

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

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

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

patch заменяет объект класса в области, где тестируемый код его ищет. Когда код выполняет создание экземпляра, фактически вызывается mock класса. Результат этого вызова находится в return_value мока класса.

Методы предполагаемого экземпляра нужно настраивать через return_value, а проверки вызова конструктора выполнять на самом mock класса. Например:

from unittest.mock import patch def load_name(User): return User().name() def test_load_name(): with patch(__name__ + ".User") as user_class: user_class.return_value.name.return_value = "Ada" assert load_name(user_class) == "Ada" user_class.assert_called_once_with()

В примере user_class — mock класса, а user_class.return_value — mock созданного экземпляра. Вызов name регистрируется у экземпляра, тогда как вызов конструктора регистрируется у mock класса.

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

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

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

Подмена класса изолирует тест полностью: отдельно проверяется факт создания клиента, параметры конструктора и взаимодействие с экземпляром. Такой тест выбран для модульного слоя, а небольшой набор интеграционных тестов без patch сохраняет проверку совместимости с реальным клиентом.

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

  1. Где настраивать метод созданного объекта?

Настройку делают через mock_класса.return_value, потому что именно этот объект возвращается при создании экземпляра. Настройка непосредственно mock_класса.method относится к mock класса и не заменяет метод его экземпляра.

  1. Что проверяет assert_called_once_with у mock класса?

Он проверяет единственный вызов самого заменённого класса и его аргументы конструктора. Он не проверяет, какие методы были вызваны у созданного mock-экземпляра; для этого проверяют объект return_value.

  1. Всегда ли подмена класса предотвращает выполнение настоящего конструктора?

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