Привет, Хабр!
Сегодня — разберёмся, почему без autospec=True
ваш безобидный @patch
из unittest.mock
может превратить зелёный репорт в мину замедленного действия.
Смысл patch()
прост: отрезаем внешний мир, подсовываем фейковый объект и гоняем логику изолированно. Но если не включить autospec
, мок превращается в пластилин — к нему прилипает любой метод, любые аргументы, и тесты радостно хлопают ладоши, даже когда в коде опечатка или нарушена сигнатура.
Что делает autospec
autospec=True
заставляет patch()
сгенерировать мок, точно повторяющий публичное API оригинального объекта:
усекает набор атрибутов строго до тех, что реально есть; попытка обратиться к вымышленному методу —
AttributeError
.дублирует сигнатуры функций и методов; лишний аргумент —
TypeError
.
Ловушка № 1: опечатка, которую никто не заметит
Пример ситуации:
# shop/payment.py
class PaymentGateway:
def charge(self, user_id: str, amount: int) -> None:
...
# shop/order.py
from shop.payment import PaymentGateway
def process_order(user_id, total):
gateway = PaymentGateway()
gateway.charge(user_id, total)
Тест на первый взгляд железобетонный:
from shop.order import process_order
from unittest.mock import patch
@patch("shop.order.PaymentGateway") # <-- autospec не указан.
def test_process_order(pg_cls_mock):
pg_inst = pg_cls_mock.return_value
pg_inst.chrage.return_value = None # опечатка: chrage
process_order("u42", 999)
pg_inst.chrage.assert_called_once() # и эта опечатка тоже
Зелёный! Но на проде опечатки нет, и метод charge()
всё же вызывается, так что юзер платит, а CI спокоен. Рефакторимся, переименовываем метод, тест всё равно зелёный. Все это из‑за того, что без autospec
любое неизвестное имя на объекте‑моке создаётся ленивым атрибутом‐моком.
Как должно быть:
@patch("shop.order.PaymentGateway", autospec=True)
def test_process_order(pg_cls_mock):
pg_inst = pg_cls_mock.return_value
# AttributeError: Mock object has no attribute 'chrage'
pg_inst.charge.return_value = None
process_order("u42", 999)
pg_inst.charge.assert_called_once()
Ловушка № 2: сигнатуры и тихие ошибки
Представим, что бизнес пришёл и сказал: «Нужна мультивалюта». Мы меняем API:
def charge(self, user_id: str, amount: int, currency: str = "RUB"): ...
Вроде всё ок, тест с autospec=False
тоже ок — ведь мок принимает любые аргументы. А вот с включённым autospec
:
pg_inst.charge.assert_called_once_with("u42", 999)
# TypeError: missing a required argument: 'currency'
Тест мгновенно подсвечивает, что мы забыли передать валюту внутрь домена.
Ловушка № 3: баги в самих тестах
Иногда мы делаем ассёрты после вызова (методология «Arrange‑Act‑Assert»). Без autospec
опечатка в методе‑ассёрте опять же проходит мимо.
pg_inst.chrge.assert_called() # тест пройдёт, хоть метода нет
С autospec=True
тут же получаем понятный AttributeError
. Тесты становятся самотестирующими.
create_autospec() и monkeypatch в фикстурах
В крупных кодовых базах с десятками модулей и зависимостей декораторы @patch(...)
быстро начинают душить читаемость. Особенно если над тестом уже висит @pytest.mark.parametrize(...)
или фикстуры тащат свои фикстуры.
Уходим в сторону pytest-monkeypatch
и create_autospec()
. И выглядит это так:
# shop/tests/test_order.py
from unittest.mock import create_autospec
from shop.payment import PaymentGateway
from shop.order import process_order
def test_order_patch_like_a_pro(monkeypatch):
fake_gateway = create_autospec(PaymentGateway)
monkeypatch.setattr("shop.order.PaymentGateway", lambda: fake_gateway)
process_order("u42", 999)
fake_gateway.charge.assert_called_once_with("u42", 999)
create_autospec
— как patch(..., autospec=True)
, но гибче: создаёт объект один раз, как мы хотим, и уже им подменяем.
monkeypatch
— просто и читаемо: «вот это было → стало вот этим». И никаких загадочных pg_cls_mock.return_value
.
AsyncMock и autospec
Хорошая практика: использовать AsyncMock
всегда, если мокаете async def
, и дополнять его autospec=True
. Это сразу поднимает две планки: сигнатуру проверяет, и runtime‑баги ловит.
Пример:
# services/notify.py
class Notifier:
async def send_email(self, user_id: str, body: str): ...
# tests/test_notify.py
from unittest.mock import AsyncMock, patch
@patch("services.notify.Notifier", new_callable=AsyncMock, autospec=True)
async def test_notify_sends_email(mock_notifier):
mock_inst = mock_notifier.return_value
await mock_inst.send_email("u42", "Hello!")
mock_inst.send_email.assert_awaited_once_with("u42", "Hello!")
Если вы:
забыли
await
→ мок отловит.передали не тот аргумент → autospec покажет.
опечатались в
send_email
→ будет AttributeError.
Когда НЕ ставить autospec
Динамические атрибуты. Если объект реально на ходу добавляет методы (например, SQLAlchemy‑модели с
getattr
для колонок) —autospec
обрежет их. Лечится моком нужного метода черезpatch.object(..., spec=True)
.Приватные вещи внутри C‑расширений. CPython не всегда отдаёт правильную сигнатуру, и
inspect.signature
может упасть. В этом случае разумно использоватьspec_set
вместоautospec
.Старые проекты на Python <= 3.6. Там были баги с
autospec
наasync def
— в таком коде лучше обновить рантайм (серьёзно, 2025 на дворе).
TL;DR‑чек‑лист
Шаг | Что делаем | Почему |
---|---|---|
1 | Добавляем | Защита от опечаток и сигнатур |
2 | Используем | Сохраняет гибкость, но проверяет наличие атрибута |
3 | В pre‑commit гоняем греп на отсутствие | Меньше человеческого фактора |
4 | Не передаём «сырые» аргументы; лучше | Заставляет явно указывать сигнатуру |
5 | При сложном DI переключаемся на | Чище читается, тот же эффект |
В итоге всё просто: если оставлять @patch
без autospec=True
— шанс выстрелить себе в ногу растёт экспоненциально с каждым коммитом. Потратьте лишние пять секунд на явный флаг, прикрутите линтер в pre‑commit.
Ну а если у вас есть интересный опыт — делитесь в комментариях.
Если тема надёжного тестирования и грамотного изоляционного подхода вам близка — возможно, вам будут интересны и другие практические разборы: от углублённого тестирования на Python до обсуждения рабочих пайплайнов для QA-инфраструктуры. Ниже — три открытых вебинара, которые стоит сохранить в закладки:
23 апреля, 19:00 — Внедрение автоматизации тестирования для QA Lead
Как выстроить процессы автотестов и не утонуть в фикстурах и flaky-тестах.30 апреля, 20:00 — Альтернативные тест раннеры. Использование stestr в API и юнит тестировании
Инструментальный взгляд на подход к масштабируемому и стабильному тестрану.22 мая, 20:00 — Тестирование кода на Python: лучшие практики для продвинутых разработчиков
Подборка приёмов, которые помогут избегать ловушек unittest и держать код в тонусе.