Как стать автором
Обновить
624.2
OTUS
Развиваем технологии, обучая их создателей

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

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров1.4K

Привет, Хабр!

Сегодня — разберёмся, почему без 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

Добавляем autospec=True ко всем patch

Защита от опечаток и сигнатур

2

Используем patch.object(..., spec=True) для динамики

Сохраняет гибкость, но проверяет наличие атрибута

3

В pre‑commit гоняем греп на отсутствие autospec

Меньше человеческого фактора

4

Не передаём «сырые» аргументы; лучше assert_called_once_with

Заставляет явно указывать сигнатуру

5

При сложном DI переключаемся на pytest-monkeypatch + create_autospec

Чище читается, тот же эффект


В итоге всё просто: если оставлять @patch без autospec=True — шанс выстрелить себе в ногу растёт экспоненциально с каждым коммитом. Потратьте лишние пять секунд на явный флаг, прикрутите линтер в pre‑commit.

Ну а если у вас есть интересный опыт — делитесь в комментариях.


Если тема надёжного тестирования и грамотного изоляционного подхода вам близка — возможно, вам будут интересны и другие практические разборы: от углублённого тестирования на Python до обсуждения рабочих пайплайнов для QA-инфраструктуры. Ниже — три открытых вебинара, которые стоит сохранить в закладки:

Теги:
Хабы:
Всего голосов 11: ↑9 и ↓2+10
Комментарии3

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS