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