2025, Dec 29 15:01

Почему and ломает with с mock.patch в Python и как делать правильно

Разбираем, почему and не объединяет менеджеры контекста в Python: как это ломает with с mock.patch и mock.patch.dict, и показываем безопасные варианты.

Во время тестирования, когда приходится править конфигурацию, легко соблазниться «склеить» несколько менеджеров контекста с помощью логического оператора. Выглядит компактно, но в Python такой прием работает не так, как ожидается. Если вы используете mock.patch.dict, чтобы подставить значения в словарь уровня модуля, и хотите ограничить патч областью одного метода, использование and внутри with незаметно сломает один из патчей.

Как возникает проблема

Подход с декораторами ведет себя предсказуемо: оба патча оборачивают вызов. Минимальный пример в таком стиле:

@mock.patch.dict("cfg.store", {"probe": 123})
@mock.patch("cfg.fetch_store", return_value=None, autospec=True)
def execute(self, patched_fetch, *args, **kwargs):
    return super().execute(*args, **kwargs)

Но переход к одному оператору with, объединенному через and, приводит к неприятностям. Фактически входит только во второй менеджер контекста; патч словаря не управляется корректно, поэтому тест не увидит подставленное значение:

def execute(self, *args, **kwargs):
    with mock.patch.dict("cfg.store", {"probe": 123}) and mock.patch(
            "cfg.fetch_store", return_value=None, autospec=True
        ) as p_fetch:
        return super().execute(*args, **kwargs)

Почему так происходит

В операторе with and не объединяет менеджеры контекста — это обычный логический оператор. Сначала вычисляется выражение mock.patch.dict(...), затем проверяется его истинность. Если оно истинно, Python вычисляет и входит только во второе выражение как в реальный менеджер контекста. В итоге патч функции активен, а патч словаря фактически не «входит» и не «выходит» как контекст — поэтому подстановка значения не срабатывает.

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

Решение

Используйте несколько менеджеров контекста, перечислив их через запятую в одном операторе with, или вложите блоки with. В обоих случаях сначала корректно активируется первый контекст, затем второй, а разворачиваются они в обратной последовательности.

def execute(self, *args, **kwargs):
    with mock.patch.dict("cfg.store", {"probe": 123}), \
         mock.patch("cfg.fetch_store", return_value=None, autospec=True) as p_fetch:
        return super().execute(*args, **kwargs)

Или, если вам ближе явная вложенность:

def execute(self, *args, **kwargs):
    with mock.patch.dict("cfg.store", {"probe": 123}):
        with mock.patch("cfg.fetch_store", return_value=None, autospec=True) as p_fetch:
            return super().execute(*args, **kwargs)

Можно также перечислить несколько менеджеров контекста в скобках вместо переноса строки с обратным слэшем. Семантика та же, а читаемость выше:

def execute(self, *args, **kwargs):
    with (
        mock.patch.dict("cfg.store", {"probe": 123}),
        mock.patch("cfg.fetch_store", return_value=None, autospec=True) as p_fetch,
    ):
        return super().execute(*args, **kwargs)

Почему это важно

Тесты, которые подкручивают конфигурацию, часто кажутся обманчиво простыми. Незаметная ошибка в использовании оператора with может частично обнулить подготовку без явных симптомов. В результате вы будете отлаживать пути выполнения с наполовину примененными патчами — это шумно и отнимает время. Понимание того, что and — не «комбайн» для менеджеров контекста, помогает избежать нестабильных тестов и сохранить строгую изоляцию.

Выводы

Когда нужно накладывать несколько патчей через менеджеры контекста, не используйте and внутри with. Либо перечисляйте менеджеры через запятую в одном with, либо вкладывайте блоки with. Подход с декораторами тоже уместен, если он вписывается в дизайн теста, но когда внутри патчируемого блока нужен доступ к атрибутам экземпляра вроде self.some_value, описанные формы с менеджерами контекста — правильный выбор. Придерживайтесь этих приемов — и патчи будут применяться предсказуемо, по порядку и гарантированно откатываться в конце блока.