Python, mocking и wrapping методы без создания объектов

Я хочу высмеять метод класса и использовать wraps, так что он действительно вызван, но я могу проверить переданные ему аргументы. Я видел в нескольких местах (например, здесь), что обычный способ сделать это следующий (адаптированный, чтобы показать мою мысль):

from unittest import TestCase
from unittest.mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    spud = Potato()

    @patch.object(Potato, 'foo', wraps=spud.foo)
    def test_something(self, mock):
        forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

Тем не менее, это создает экземпляр класса Potato, чтобы связать макет с методом экземпляра spud.foo.

Мне нужно, чтобы издеваться над методом foo во всех экземплярах Potato и обернуть их вокруг оригинальных методов. Т.е. мне нужно следующее:

from unittest import TestCase
from unittest.mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    @patch.object(Potato, 'foo', wraps=Potato.foo)
    def test_something(self, mock):
        self.spud = Potato()
        forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

Это, конечно, не работает. Я получаю сообщение об ошибке:

TypeError: foo() missing 1 required positional argument: 'self'

Однако он работает, если wraps не используются, поэтому проблема не в макете, а в том, как она вызывает завернутую функцию. Например, это работает (но, конечно, мне пришлось "подделать" возвращаемое значение, потому что теперь Potato.foo никогда не запускается):

from unittest import TestCase
from unittest.mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    @patch.object(Potato, 'foo', return_value=42)#, wraps=Potato.foo)
    def test_something(self, mock):
        self.spud = Potato()
        forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

Это работает, но он не запускает исходную функцию, которую мне нужно запустить, потому что возвращаемое значение используется в другом месте (и я не могу подделать его из теста).

Это можно сделать?

Примечание. Фактическая причина моих потребностей заключается в том, что я тестирую приложение api для отдыха с веб-тестированием. Из тестов я выполняю некоторые запросы wsgi к некоторым путям, а моя среда создает некоторые классы и использует их методы для выполнения запроса. Я хочу, чтобы захватить параметры, отправленные на эти методы, чтобы сделать некоторые asserts о них в моих тестах.

Ответы

Ответ 1

Вы контролируете создание экземпляров Potato или, по крайней мере, имеете доступ к этим экземплярам после их создания? Вы должны, иначе вы не сможете проверить конкретные списки аргументов. Если это так, вы можете обернуть методы отдельных экземпляров, используя

spud = dig_out_a_potato()
with mock.patch.object(spud, "foo", wraps=spud.foo) as mock_spud:
  # do your thing.
  mock_spud.assert_called...

Ответ 2

Короче говоря, вы не можете сделать это, используя только экземпляры Mock.

patch.object создает Mock для указанного экземпляра (Potato), т.е. он заменяет Potato.foo одним Mock в момент его Potato.foo. Следовательно, нет способа передать экземпляры в Mock поскольку mock создается до создания каких-либо экземпляров. Насколько мне известно, получение информации об экземпляре в Mock во время выполнения также очень сложно.

Проиллюстрировать:

from unittest.mock import MagicMock

class MyMock(MagicMock):
    def __init__(self, *a, **kw):
        super(MyMock, self).__init__(*a, **kw)
        print('Created Mock instance a={}, kw={}'.format(a,kw))

with patch.object(Potato, 'foo', new_callable=MyMock, wrap=Potato.foo):
    print('no instances created')
    spud = Potato()
    print('instance created')

Выход:

Created Mock instance a=(), kw={'name': 'foo', 'wrap': <function Potato.foo at 0x7f5d9bfddea0>}
no instances created
instance created

Я бы посоветовал сделать обезьяны-патчи для вашего класса, чтобы добавить Mock в правильное место.

from unittest.mock import MagicMock

class PotatoTest(TestCase):
    def test_something(self):

        old_foo = Potato.foo
        try:
            mock = MagicMock(wraps=Potato.foo, return_value=42)
            Potato.foo = lambda *a,**kw: mock(*a, **kw)

            self.spud = Potato()
            forty_two = self.spud.foo(n=40)
            mock.assert_called_once_with(self.spud, n=40) # Now needs self instance
            self.assertEqual(forty_two, 42)
        finally:
            Potato.foo = old_foo

Обратите внимание, что вы используете called_with проблематично, так как вы вызываете свои функции с экземпляром.