Могу ли я имитировать возврат функции, который вызывается в рамках другого вызова функции в тесте Python?

#python #python-unittest

#python #python-unittest

Вопрос:

Есть ли способ имитировать возврат функции, который вызывается в рамках другого вызова функции? Например:

 def bar():
    return "baz"

def foo():
    return bar()

class Tests(unittest.TestCase):
    def test_code(self):
        # hijack bar() here to return "bat" instead
        assert(foo() == "bat")
  

Я пробовал использовать @mock.patch , но обнаружил, что это позволяет мне имитировать только функцию, которую я вызываю, а не функцию, которая будет вызвана в результате вызова другой функции.

Ответ №1:

Универсальный патч

unittest.mock.patch делает именно то, что мой другой ответ предложил из коробки. Вы можете добавить столько @patch аннотаций, сколько вам нужно, и выбранные вами объекты будут исправлены:

 from unittest.mock import patch

def bar():
    return "baz"

def foo():
    return bar()

class Tests(unittest.TestCase):
    @patch(__name__   '.bar', lambda: 'bat')
    def test_code(self):
        assert(foo() == "bat")
  

В этой конфигурации функция bar будет восстановлена после test_code завершения. Если вы хотите, чтобы один и тот же патч применялся ко всем тестовым примерам в вашем классе, аннотируйте весь класс:

 @patch(__name__   '.bar', lambda: 'bat')
class Tests(unittest.TestCase):
    def test_code(self):
        assert(foo() == "bat")
  

Глобальные исправления

Вы также можете unittest.mock.patch.dict получить тот же результат в своем глобальном пространстве имен:

 class Tests(unittest.TestCase):
    @patch.dict(globals(), {'bar': lambda: 'bat'})
    def test_code(self):
        assert(foo() == "bat")
  

Комментарии:

1. Это здорово. Не представлял, что вы могли бы сделать это с помощью Patch. Спасибо за вашу помощь

Ответ №2:

Вы можете написать диспетчер контекста для временной замены объектов в глобальном пространстве имен:

 class Hijack:
    def __init__(self, name, replacement, namespace):
        self.name = name
        self.replacement = replacement
        self.namespace = namespace

    def __enter__(self):
        self.original = self.namespace[self.name]
        self.namespace[self.name] = self.replacement

    def __exit__(self, *args):
        self.namespace[self.name] = self.original
  

Вы можете вызвать свою фиктивную функцию с помощью взломанного метода:

 def bar():
    return "baz"

def bar_mock():
    return "bat"

def foo():
    return bar()

class Tests(unittest.TestCase):
    def test_code(self):
        with Hijack('bar', bar_mock, globals()):
            assert(foo() == "bat")
  

Это довольно общий подход, который можно использовать вне модульного тестирования. На самом деле, довольно просто обобщить это для работы с любым изменяемым объектом, который может быть представлен в виде некоторого отображения:

 class Hijack:
    def __init__(self, name, replacement, namespace, getter=None, setter=None):
        self.name = name
        self.replacement = replacement
        self.namespace = namespace
        self.getter = type(namespace).__getitem__ if getter is None else getter
        self.setter = type(namespace).__setitem__ if setter is None else setter

    def __enter__(self):
        self.original = self.getter(self.namespace, self.name)
        self.setter(self.namespace, self.name, self.replacement)

    def __exit__(self, *args):
        self.setter(self.namespace, self.name, self.original)
  

Для классов и других объектов вы бы использовали getter=getattr и setter=setattr . Для ситуаций, когда None предпочтительнее KeyError , вы можете использовать getter=dict.get и т.д.

Ответ №3:

Также вы могли бы использовать seattr метод monkeypatch fixture pytest. Для первых аргументов он принимает объект, подлежащий исправлению, или строка будет интерпретироваться как путь импорта с пунктиром, причем последняя часть является именем атрибута:

 # foo_module.py
def bar():
    return "baz"


def foo():
    return bar()

  
 # test_foo.py
from foo_module import foo


def test_foo(monkeypatch):
    monkeypatch.setattr('foo_module.bar', lambda: 'bat')
    assert foo() == "bat"
  

Комментарии:

1. Интересно, я попробую это. Будет ли проблема придерживаться описанного выше подхода к исправлению, но это полезно знать в любом случае. Спасибо