Вам нужно подменить сетевой вызов в тесте, но функция уже сохранена в локальном имени модуля. Какой вызов ф...

Вам нужно подменить сетевой вызов в тесте, но функция уже сохранена в локальном имени модуля. Какой вызов фактически выполнится?

from unittest.mock import patch


def fetch_data():
    return "real"


controller_fetch_data = fetch_data


def load():
    return controller_fetch_data()


def test_load():
    with patch(__name__ + ".fetch_data", return_value="fake"):
        assert load() == ???
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Вызов фактически вернёт "real", поэтому проверка с "fake" завершится ошибкой. Подменено имя fetch_data, но функция load ищет вызываемый объект по имени controller_fetch_data, которое ранее уже ссылалось на исходную функцию.

Чтобы подмена сработала, нужно патчить имя в пространстве имён потребителя: controller_fetch_data, а не исходное имя fetch_data.

Исторический контекст

Mocking появился как практический способ изолировать тестируемый код от сети, файловой системы, баз данных и других медленных или недетерминированных зависимостей. В Python подмена обычно выполняется не «вообще для функции», а для конкретного имени, через которое код получает доступ к этой функции.

Это связано с моделью импорта и связывания имён Python: импорт создаёт ссылку в пространстве имён текущего модуля. Поэтому корректная цель подмены определяется местом поиска объекта во время выполнения.

Постановка проблемы

В примере controller_fetch_data получает ссылку на fetch_data при выполнении присваивания. Позднее patch заменяет только значение глобального имени fetch_data; уже созданная ссылка controller_fetch_data от этого не меняется.

Если в реальном тесте патчить не то пространство имён, внешний вызов может неожиданно уйти в сеть, обратиться к настоящей базе данных или выполнить другой побочный эффект. Такой тест либо падает, либо ошибочно считается изолированным.

Подробное решение

Правило формулируется так: патчить нужно там, где зависимость ищется, а не там, где она первоначально объявлена. В данном коде правильная цель — имя_модуля.controller_fetch_data.

from unittest.mock import patch def fetch_data(): return "real" controller_fetch_data = fetch_data def load(): return controller_fetch_data() def test_load(): with patch(__name__ + ".controller_fetch_data", return_value="fake"): assert load() == "fake"

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

Если код потребителя использует import app.service, а затем вызывает app.service.fetch_data(), патчить можно app.service.fetch_data. Если же он использует from app.service import fetch_data, патчить нужно app.controller.fetch_data — имя, созданное в модуле потребителя.

Компромисс такого подхода — тест зависит от способа импорта и структуры модулей. Это делает ошибочную перестановку импортов потенциально ломающей тесты, но одновременно гарантирует, что проверяется именно реальная точка взаимодействия потребителя с зависимостью.

Ситуация из практики

Сервис обращался к платёжному шлюзу через from payments.client import charge. Тест патчил payments.client.charge, но при запуске вызывал настоящую интеграцию: контроллер уже имел собственную ссылку charge.

Рассматривались два варианта. Патчить исходный модуль было проще визуально, но это не влияло на импортированный алиас. Переписать код на постоянное обращение через payments.client.charge уменьшало риск неверной цели, однако меняло стиль и устройство production-кода.

Выбрали патч controller.charge, то есть имя в модуле-потребителе. Тест стал изолированным, а контракт контроллера сохранился; дополнительно в ревью зафиксировали правило выбора цели патча.

Что кандидаты часто упускают

  1. Что произойдёт при from module import func, если затем патчить module.func?

    Импорт создаёт отдельное имя func в модуле-потребителе. Замена module.func не переназначает это имя, поэтому потребитель продолжит вызывать старый объект. Нужно патчить consumer.func, если именно так зависимость вызывается.

  2. Почему патч метода класса часто нужно ставить на класс, а не на уже созданный объект?

    При обращении instance.method() Python обычно получает метод через атрибут класса и выполняет связывание экземпляра. Если код ищет метод через класс, патчируют Class.method; если метод заранее сохранён в атрибуте экземпляра или локальной переменной, нужно учитывать уже созданную ссылку и патчить место её фактического использования.

  3. Что изменится, если зависимость передавать в функцию параметром?

    Тогда функция использует переданный объект, а не ищет глобальное имя. Например, load(fetch=fetch_data) при вызове с load(fake_fetch) тестирует подмену напрямую, без patch; это обычно делает зависимость явнее и уменьшает связанность теста со структурой импортов. Однако изменение сигнатуры может быть избыточным для простой внутренней зависимости.