Python mock:包装实例方法
Python mock: wrap instance method
我想要什么:
确保在 with
语句内创建的 Foo
的所有实例都通过 wraps=Foo.foo
将其 foo
实例方法包装在 MagicMock 中。我想要这个的原因是我可以在方法 foo
上跟踪 call_count
创建的所有 Foo
实例。这么一说好像有点不太可能...
>>> from mock import patch
...
... class Foo(object):
...
... def foo(self):
... return "foo"
...
... with patch("__main__.Foo.foo", wraps=Foo.foo) as m:
... foo = Foo()
... print(foo.foo())
Traceback (most recent call last):
File "a.py", line 12, in <module>
print(foo.foo())
File "/disk/software/lib/python27/mock/mock.py", line 1062, in __call__
return _mock_self._mock_call(*args, **kwargs)
File "/disk/software/lib/python27/mock/mock.py", line 1132, in _mock_call
return self._mock_wraps(*args, **kwargs)
TypeError: unbound method foo() must be called with Foo instance as first argument (got nothing instead)
问题
模拟的 foo
方法未绑定到通过 foo = Foo()
创建的 foo
实例,因为它包装了未绑定的方法 Foo.foo
。有谁知道如何确保模拟方法绑定到实例?
我已经知道的:
>>> foo = Foo()
... with patch.object(foo, "foo", wraps=foo.foo) as m:
... print(foo.foo())
"foo"
但这不满足我的约束,即对象必须在 patch
上下文中实例化。
我上面提出的错误解决方案的问题
with patch("__main__.Foo.foo", wraps=Foo.foo) as m:
...
是Foo
上的foo
方法被mock了,把unbound方法Foo.foo
包裹起来了,自然不行,因为unbound方法Foo.foo
不知道稍后调用时它附加到哪个实例。
我能想到的最简单的解决方案
from mock import patch, MagicMock
class Foo:
def foo(self):
return "foo"
class MockFoo(Foo):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Every instance of MockFoo will now have its `foo` method
# wrapped in a MagicMock
self.foo = MagicMock(wraps=self.foo)
with patch("__main__.Foo", MockFoo) as m:
foo = Foo()
print(foo.foo())
assert foo.foo.call_count == 1
这对 Python 模拟来说太讨厌了,所以我最终构建了一个自定义补丁实现(如果需要,可以扩展其他功能)。
import contextlib
class Patcher:
UNCHANGED_RET = object()
def __init__(self):
self.call_count = 0
self.return_value = Patcher.UNCHANGED_RET
@contextlib.contextmanager
def patch(klass, method_name):
patcher = Patcher()
orig_method = getattr(klass, method_name)
def new_method(myself, *args, **kwargs):
patcher.call_count += 1
orig_return_value = orig_method(myself, *args, **kwargs)
if patcher.return_value != Patcher.UNCHANGED_RET:
return patcher.return_value
return orig_return_value
setattr(klass, method_name, new_method)
yield patcher
setattr(klass, method_name, orig_method)
使用如下:
class MyClass:
def f(self):
return 42
x = MyClass()
with patch(MyClass, 'f') as f_patcher:
y = MyClass() # inside or outside -- does not matter
assert x.f() == 42
assert f_patcher.call_count == 1
f_patcher.return_value = 7
assert y.f() == 7
assert f_patcher.call_count == 2
在处理 Apport 时,我 运行 遇到了完全相同的问题。我尝试了多种方法并提出了这个解决方案,这是最优雅和可读性最高的解决方案:
# 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
wrap_object
的单元测试:
class Multiply:
"""Test class for wrap_object test cases."""
def __init__(self, multiplier):
self.multiplier = multiplier
def multiply(self, x: int) -> int:
return x * self.multiplier
class TestWrapObject(unittest.TestCase):
def test_wrap_object_with_statement(self):
with wrap_object(Multiply, "__init__") as mock:
m = Multiply(7)
self.assertEqual(m.multiply(6), 42)
mock.assert_called_once_with(7)
然后您可以在初始示例中使用以下代码:
>>> with wrap_object(Foo, "foo") as m:
... foo = Foo()
... print(foo.foo())
...
foo
>>> m.assert_called_once_with()
>>> m.call_count
1
>>>
我想要什么:
确保在 with
语句内创建的 Foo
的所有实例都通过 wraps=Foo.foo
将其 foo
实例方法包装在 MagicMock 中。我想要这个的原因是我可以在方法 foo
上跟踪 call_count
创建的所有 Foo
实例。这么一说好像有点不太可能...
>>> from mock import patch
...
... class Foo(object):
...
... def foo(self):
... return "foo"
...
... with patch("__main__.Foo.foo", wraps=Foo.foo) as m:
... foo = Foo()
... print(foo.foo())
Traceback (most recent call last):
File "a.py", line 12, in <module>
print(foo.foo())
File "/disk/software/lib/python27/mock/mock.py", line 1062, in __call__
return _mock_self._mock_call(*args, **kwargs)
File "/disk/software/lib/python27/mock/mock.py", line 1132, in _mock_call
return self._mock_wraps(*args, **kwargs)
TypeError: unbound method foo() must be called with Foo instance as first argument (got nothing instead)
问题
模拟的 foo
方法未绑定到通过 foo = Foo()
创建的 foo
实例,因为它包装了未绑定的方法 Foo.foo
。有谁知道如何确保模拟方法绑定到实例?
我已经知道的:
>>> foo = Foo()
... with patch.object(foo, "foo", wraps=foo.foo) as m:
... print(foo.foo())
"foo"
但这不满足我的约束,即对象必须在 patch
上下文中实例化。
我上面提出的错误解决方案的问题
with patch("__main__.Foo.foo", wraps=Foo.foo) as m:
...
是Foo
上的foo
方法被mock了,把unbound方法Foo.foo
包裹起来了,自然不行,因为unbound方法Foo.foo
不知道稍后调用时它附加到哪个实例。
我能想到的最简单的解决方案
from mock import patch, MagicMock
class Foo:
def foo(self):
return "foo"
class MockFoo(Foo):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Every instance of MockFoo will now have its `foo` method
# wrapped in a MagicMock
self.foo = MagicMock(wraps=self.foo)
with patch("__main__.Foo", MockFoo) as m:
foo = Foo()
print(foo.foo())
assert foo.foo.call_count == 1
这对 Python 模拟来说太讨厌了,所以我最终构建了一个自定义补丁实现(如果需要,可以扩展其他功能)。
import contextlib
class Patcher:
UNCHANGED_RET = object()
def __init__(self):
self.call_count = 0
self.return_value = Patcher.UNCHANGED_RET
@contextlib.contextmanager
def patch(klass, method_name):
patcher = Patcher()
orig_method = getattr(klass, method_name)
def new_method(myself, *args, **kwargs):
patcher.call_count += 1
orig_return_value = orig_method(myself, *args, **kwargs)
if patcher.return_value != Patcher.UNCHANGED_RET:
return patcher.return_value
return orig_return_value
setattr(klass, method_name, new_method)
yield patcher
setattr(klass, method_name, orig_method)
使用如下:
class MyClass:
def f(self):
return 42
x = MyClass()
with patch(MyClass, 'f') as f_patcher:
y = MyClass() # inside or outside -- does not matter
assert x.f() == 42
assert f_patcher.call_count == 1
f_patcher.return_value = 7
assert y.f() == 7
assert f_patcher.call_count == 2
在处理 Apport 时,我 运行 遇到了完全相同的问题。我尝试了多种方法并提出了这个解决方案,这是最优雅和可读性最高的解决方案:
# 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
wrap_object
的单元测试:
class Multiply:
"""Test class for wrap_object test cases."""
def __init__(self, multiplier):
self.multiplier = multiplier
def multiply(self, x: int) -> int:
return x * self.multiplier
class TestWrapObject(unittest.TestCase):
def test_wrap_object_with_statement(self):
with wrap_object(Multiply, "__init__") as mock:
m = Multiply(7)
self.assertEqual(m.multiply(6), 42)
mock.assert_called_once_with(7)
然后您可以在初始示例中使用以下代码:
>>> with wrap_object(Foo, "foo") as m:
... foo = Foo()
... print(foo.foo())
...
foo
>>> m.assert_called_once_with()
>>> m.call_count
1
>>>