Почему @patch из unittest.mock ломает вам тесты, если не указать autospec=True

Привет, Хабр!
Сегодня — разберёмся, почему без 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 и держать код в тонусе.
Habrahabr.ru прочитано 34213 раз
