2025, Sep 21 05:02
Как не сломать мок в pytest и pytest-mock: настраиваем return_value правильно
Разбираем типичную ошибку в pytest и pytest-mock: почему dep.do_it().return_value не работает и как правильно настроить dep.do_it.return_value. Пример.
При модульном тестировании класса, который передает работу другому классу, легко неверно настроить мок и потом гадать, почему тест падает. Распространенная ловушка в pytest и pytest-mock — выставлять возвращаемое значение не на самом методе мока, а на результате его вызова. Ниже — минимальный пример сбоя и точное изменение, которое заставит тест вести себя как задумано.
Пример настройки
class Dependency:
    def do_it(self):
        return 1
class Service:
    def __init__(self, dep):
        self.dep = dep
    def run(self):
        return self.dep.do_it()
def test_run():
    dep = Dependency()
    svc = Service(dep)
    assert svc.run() == 1
def test_run_with_mock(mocker):
    dep = mocker.Mock()
    svc = Service(dep)
    # неверная строка показана ниже и исправлена далее
    # dep.do_it().return_value = 1
    # правильная строка:
    dep.do_it.return_value = 1
    assert svc.run() == 1
    dep.do_it.assert_called_once_with()
Что пошло не так
Падающая версия устанавливает возвращаемое значение объекта, полученного вызовом мока, а не самого метода на мок-объекте. Конкретнее, виновата эта строка:
dep.do_it().return_value = 1
Это выражение вызывает метод мока, а затем пытается настроить возвращаемое значение того, что вернул этот вызов. В итоге сам метод, который действительно вызывает тестируемый код, остается не сконфигурированным. Поэтому кажется, что мок «не вызывался» или ведет себя не так, как ожидается. Более того, если вы по ошибке настраиваете возвращаемое значение результата вызова, утверждение вроде повторного вызова этого результата может даже пройти — наглядное напоминание, насколько важно, чтобы тестовые двойники повторяли реальный интерфейс зависимостей.
Как исправить
Настраивайте возвращаемое значение на самом замоканном методе, а не на результате его вызова:
dep.do_it.return_value = 1
После этого тест должен пройти, и вы можете дополнительно проверить, что взаимодействие произошло ровно один раз и без аргументов:
dep.do_it.assert_called_once_with()
Почему этот нюанс важен
Мокирование — это про определение контракта между тестируемым объектом и его коллабораторами. Если вы случайно задаёте возвращаемое значение не методу, а результату вызова, вы больше не тестируете тот же контракт, который использует рабочий код. Такой тонкий рассинхрон маскирует ошибки или, что хуже, создаёт ложное чувство уверенности. Он также показывает, почему так важно, чтобы поверхность мока совпадала с реальной зависимостью. Этот принцип одинаково работает и с фикстурой mocker из pytest-mock, и при прямом создании unittest.mock.Mock(): поведение вокруг return_value идентично.
Итоги
Когда подменяете возвращаемое значение метода у мока, задавайте его на атрибуте метода, а не на результате его вызова. Следите за проверками взаимодействия, такими как assert_called_once_with(), чтобы убеждаться, что мок используется ровно так, как задумано. Если интерфейс зависимости очень простой, рассмотрите возможность передать тривиальную тестовую реализацию с той же сигнатурой метода — это делает контракт явным и поддерживает читабельность тестов. В любом случае убедитесь, что используемый тестовый двойник зеркалирует зависимость, которую ожидает ваш боевой код.
Статья основана на вопросе на StackOverflow от user1604008 и ответе wim.