Откуда create autospec берёт сигнатуру метода экземпляра и почему при вызове такого mock метода не передают...

Откуда create_autospec берёт сигнатуру метода экземпляра и почему при вызове такого mock-метода не передают self вручную?

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

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

create_autospec извлекает сигнатуру метода из реального класса или объекта с помощью интроспекции Python. Если mock представляет метод экземпляра, он имитирует уже связанный метод, поэтому self передавать вручную не нужно: в проверяемую сигнатуру входят только пользовательские аргументы.

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

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

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

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

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

При использовании create_autospec важно понимать границу между методом класса и связанным методом экземпляра. Ошибочное ручное добавление self приводит к ложной ошибке о лишнем аргументе и маскирует правильное поведение тестируемого кода.

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

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

create_autospec воспроизводит именно этот пользовательский интерфейс. Поэтому mock экземпляра принимает аргументы так, как их принимает обычный объект: без self.

from unittest.mock import create_autospec class Service: def send(self, message, *, urgent=False): pass service = create_autospec(Service, instance=True) service.send("готово", urgent=True) service.send.assert_called_once_with("готово", urgent=True) service.send("готово", True) # TypeError: urgent — только именованный аргумент

Вызов service.send("готово", urgent=True) корректен, а позиционная передача второго аргумента нарушает исходную сигнатуру. Такая проверка выполняется самим mock до фактической записи вызова.

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

Если autospec создаётся для класса без instance=True, результат представляет mock класса. Его return_value обычно используется как mock создаваемого экземпляра; методы этого результата также имеют сигнатуры экземпляра. Для уже существующего объекта можно передать сам объект в create_autospec.

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

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

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

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

Выбран был create_autospec для клиента внешнего API. Он сохранил изоляцию от сети и одновременно начал немедленно выявлять неправильные имена, количество и способ передачи аргументов. В результате изменение контракта стало заметно на этапе запуска unit-тестов, хотя проверка типов и реального ответа API по-прежнему осталась задачей интеграционных тестов.

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

  1. Допустит ли autospec значение неправильного типа, если сигнатура соблюдена?

Да. Autospec проверяет вызов, а не аннотации типов и не содержимое аргументов. Если метод ожидает строку, передача числа может быть принята, если количество и способ передачи аргументов соответствуют сигнатуре. Для проверки типов нужны отдельные средства, например статический анализ, а для бизнес-правил — явные проверки в тесте.

  1. Проверяет ли autospec, что метод действительно возвращает значение нужного типа?

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

  1. Что произойдёт, если autospec создаётся на уже подменённом объекте?

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