python mock - 不干扰实现的方法打补丁
有没有一种简单的方法可以对一个对象进行补丁,这样在你的测试案例中就能使用到 assert_call*
这些辅助工具,而不需要真的去移除那个动作呢?
举个例子,我该怎么修改 @patch
这一行,才能让下面的测试通过:
from unittest import TestCase
from 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')
def test_something(self, mock):
spud = Potato()
forty_two = spud.foo(n=40)
mock.assert_called_once_with(n=40)
self.assertEqual(forty_two, 42)
我可能可以用 side_effect
来搞定这个,但我希望能有一种更好的方法,能够在所有函数、类方法、静态方法和未绑定的方法上都能一样好用。
6 个回答
我用了一种稍微不同的方法,因为在我看来,模拟比修改更好。
from unittest.mock import create_autospec
mocked_method = create_autospec(
spec=my_method,
spec_set=True,
# Will implement a real behavior rather than return a Mock instance
side_effect=*a, **kw: my_method.do_something(*a, **kw))
mocked_object.do_something()
mocked_object.assert_called_once()
你提到的问题和这个Python mock: wrap instance method的内容是一样的。我在这个链接中给出的解决方案可以这样应用:把wrap_object
放到某个地方,比如放在wrap_object.py
文件里:
# Copyright (C) 2022, Benjamin Drung <bdrung@posteo.de>
#
# 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:
spud = Potato()
forty_two = spud.foo(n=40)
mock.assert_called_once_with(n=40)
self.assertEqual(forty_two, 42)
对于那些不介意使用 side_effect
的人,这里有一个解决方案,优点有几个:
- 使用了装饰器的语法
- 修补了一个未绑定的方法,我觉得这样更灵活
- 在断言中需要包含实例
class PotatoTest(TestCase):
@patch.object(Potato, 'foo', side_effect=Potato.foo, autospec=True)
def test_something(self, mock):
spud = Potato()
forty_two = spud.foo(n=40)
mock.assert_called_once_with(spud, n=40)
self.assertEqual(forty_two, 42)
这个回答是针对用户Quuxplusone在悬赏中提到的额外需求的。
对我来说,最重要的是它能与
@patch.mock
一起使用,也就是说,我不需要在创建Potato
实例(在这个例子中是spud
)和调用spud.foo
之间插入任何代码。我需要spud
一开始就用一个模拟的foo
方法来创建,因为我无法控制spud
被创建的地方。
上面描述的用例可以通过使用装饰器来轻松实现:
import unittest
import unittest.mock # Python 3
def spy_decorator(method_to_decorate):
mock = unittest.mock.MagicMock()
def wrapper(self, *args, **kwargs):
mock(*args, **kwargs)
return method_to_decorate(self, *args, **kwargs)
wrapper.mock = mock
return wrapper
def spam(n=42):
spud = Potato()
return spud.foo(n=n)
class Potato(object):
def foo(self, n):
return self.bar(n)
def bar(self, n):
return n + 2
class PotatoTest(unittest.TestCase):
def test_something(self):
foo = spy_decorator(Potato.foo)
with unittest.mock.patch.object(Potato, 'foo', foo):
forty_two = spam(n=40)
foo.mock.assert_called_once_with(n=40)
self.assertEqual(forty_two, 42)
if __name__ == '__main__':
unittest.main()
如果被替换的方法接受可变参数,并且这些参数在测试中会被修改,你可能希望在spy_decorator
中用CopyingMock
替代MagicMock
。
*这是一个来自于文档的示例,我已经将其发布为copyingmock库。
这个方案和你的类似,但使用了 wraps
:
def test_something(self):
spud = Potato()
with patch.object(Potato, 'foo', wraps=spud.foo) as mock:
forty_two = spud.foo(n=40)
mock.assert_called_once_with(n=40)
self.assertEqual(forty_two, 42)
根据文档的说明:
wraps:这是一个用于模拟对象的包装项。如果 wraps 不是 None,那么调用这个模拟对象时,会把调用传递给被包装的对象(并返回真实的结果)。在模拟对象上访问属性时,会返回一个模拟对象,这个模拟对象会包装被包装对象的相应属性(所以如果你尝试访问一个不存在的属性,就会抛出一个 AttributeError 错误)。
class Potato(object):
def spam(self, n):
return self.foo(n=n)
def foo(self, n):
return self.bar(n)
def bar(self, n):
return n + 2
class PotatoTest(TestCase):
def test_something(self):
spud = Potato()
with patch.object(Potato, 'foo', wraps=spud.foo) as mock:
forty_two = spud.spam(n=40)
mock.assert_called_once_with(n=40)
self.assertEqual(forty_two, 42)