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

Разработчик создаёт Mock со спецификацией класса и затем присваивает ему новый атрибут. Какое ограничение д...

Разработчик создаёт Mock со спецификацией класса и затем присваивает ему новый атрибут. Какое ограничение даёт spec_set по сравнению с spec?

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

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

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

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

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

Параметры spec и spec_set появились как средства связать mock с интерфейсом реального объекта. Они помогают обнаруживать расхождение теста с кодом ещё во время выполнения теста, а не после изменения реализации.

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

Предположим, у класса есть метод send, но в тесте из-за опечатки создаётся атрибут senс с похожим написанием. Без ограничения mock может принять такую ошибку: тест настроит несуществующий атрибут и продолжит выполняться.

spec не полностью решает проблему, если тест присваивает атрибуту значение. Поэтому выбор между spec и spec_set влияет на строгость проверки интерфейса и на совместимость тестов с объектами, которые динамически расширяются.

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

При создании mock с spec набор допустимых атрибутов берётся из переданного объекта или класса. Попытка получить неизвестный атрибут завершается ошибкой AttributeError, но присваивание нового атрибута обычно разрешено.

spec_set использует тот же принцип проверки, но запрещает и чтение, и присваивание атрибутов, отсутствующих в спецификации. Поэтому он лучше защищает от опечаток при настройке mock-объекта.

from unittest.mock import Mock class Sender: def send(self, data): pass with_spec = Mock(spec=Sender) with_spec.senс = Mock() # присваивание разрешено strict = Mock(spec_set=Sender) try: strict.senс = Mock() # AttributeError except AttributeError: print("Неизвестный атрибут")

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

Спецификация проверяет наличие атрибутов, но не гарантирует корректность бизнес-логики и не заменяет проверку реального объекта. Кроме того, если передать класс, атрибуты, создаваемые только в __init__, могут отсутствовать в спецификации класса; для таких случаев иногда нужен экземпляр или другой способ формирования интерфейса.

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

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

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

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

Рассматривались два варианта. spec ограничивал получение неизвестных атрибутов, но не защищал от ошибочных присваиваний при настройке; отказ от спецификации сохранял гибкость, однако оставлял риск тихих опечаток. В качестве решения выбрали spec_set для mock-объектов внешних клиентов, а для проверки сигнатур — autospec там, где это требовалось.

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

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

  1. Запрещает ли spec_set изменять существующий атрибут?

Нет. spec_set запрещает создавать или получать атрибуты, которых нет в спецификации, но не запрещает присваивать новое значение уже допустимому атрибуту. Например, существующий метод mock можно заменить другим mock-объектом или настроить через return_value.

  1. Проверяет ли spec_set правильность аргументов метода?

Нет, его основная задача — контроль имён атрибутов. Наличие метода в спецификации ещё не означает, что mock проверит число и имена переданных аргументов. Для этого нужен autospec или создание mock с явно заданной сигнатурой; даже тогда проверяется форма вызова, а не корректность результата бизнес-операции.

  1. Почему spec_set, созданный по классу, может не принять атрибут экземпляра?

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