我想模拟类的方法并使用
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)
这可行,但它不运行原始函数,我需要运行该函数,因为返回值在其他地方使用(并且我无法从测试中伪造它)。
可以做吗?
注意 我的需求背后的实际原因是我正在使用 webtest 测试rest api。在测试中,我对某些路径执行了一些 wsgi 请求,并且我的框架实例化了一些类并使用它们的方法来满足请求。我想捕获发送到这些方法的参数,以便在我的测试中对它们进行一些asserts
。
Mock
实例来做到这一点。
patch.object
为指定实例(Potato)创建
Mock
,即在调用时用单个 Mock 替换
Potato.foo
。因此,无法将实例传递给
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
是有问题的。
python模拟 - 修补方法而不妨碍实现相同。 https://stackoverflow.com/a/72446739/9230828 实现你想要的(除了它使用 with 语句而不是装饰器)。 wrap_object.py
:
# Copyright (C) 2022, Benjamin Drung <[email protected]>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import contextlib
import typing
import unittest.mock
@contextlib.contextmanager
def wrap_object(
target: object, attribute: str
) -> typing.Generator[unittest.mock.MagicMock, None, None]:
"""Wrap the named member on an object with a mock object.
wrap_object() can be used as a context manager. Inside the
body of the with statement, the attribute of the target is
wrapped with a :class:`unittest.mock.MagicMock` object. When
the with statement exits the patch is undone.
The instance argument 'self' of the wrapped attribute is
intentionally not logged in the MagicMock call. Therefore
wrap_object() can be used to check all calls to the object,
but not differentiate between different instances.
"""
mock = unittest.mock.MagicMock()
real_attribute = getattr(target, attribute)
def mocked_attribute(self, *args, **kwargs):
mock.__call__(*args, **kwargs)
return real_attribute(self, *args, **kwargs)
with unittest.mock.patch.object(target, attribute, mocked_attribute):
yield mock
然后你可以编写以下单元测试:
from unittest import TestCase
from wrap_object import wrap_object
class Potato:
def foo(self, n):
return self.bar(n)
def bar(self, n):
return n + 2
class PotatoTest(TestCase):
def test_something(self):
with wrap_object(Potato, 'foo') as mock:
self.spud = Potato()
forty_two = self.spud.foo(n=40)
mock.assert_called_once_with(n=40)
self.assertEqual(forty_two, 42)
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...
Potato
实例,这为您提供了您正在寻找的控件:
from unittest import TestCase
from unittest.mock import patch
from functools import partial
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=partial(Potato.foo, spud))
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
的 all
实例,同时保持
wraps
的使用