Привет, Хабр!
Сегодня — разберёмся, почему без 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 и держать код в тонусе.
ссылка на оригинал статьи https://habr.com/ru/articles/901534/
Добавить комментарий