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, описанные формы с менеджерами контекста — правильный выбор. Придерживайтесь этих приемов — и патчи будут применяться предсказуемо, по порядку и гарантированно откатываться в конце блока.