如何在 isinstance 测试中模拟补丁 a class?
How can I mock patch a class used in an isinstance test?
我想测试功能is_myclass
。请帮助我了解如何编写成功的测试。
def is_myclass(obj):
"""This absurd stub is a simplified version of the production code."""
isinstance(obj, MyClass)
MyClass()
文档
unittest.mock 的 Python 文档说明了解决 isinstance
问题的三种方法:
- 将
spec
参数设置为真正的class。
- 将真实的 class 分配给
__class__
属性。
- 在真实class的补丁中使用
spec
。
__class__
Normally the __class__
attribute of an object will return its type. For a mock object with a spec, __class__
returns the spec class instead. This allows mock objects to pass isinstance() tests for the object they are replacing / masquerading as:
>>> mock = Mock(spec=3)
>>> isinstance(mock, int)
True
__class__
is assignable to, this allows a mock to pass an isinstance()
check without forcing you to use a spec:
>>> mock = Mock()
>>> mock.__class__ = dict
>>> isinstance(mock, dict)
True
[...]
If you use spec
or spec_set
and patch()
is replacing a class, then the return value of the created mock will have the same spec.
>>> Original = Class
>>> patcher = patch('__main__.Class', spec=True)
>>> MockClass = patcher.start()
>>> instance = MockClass()
>>> assert isinstance(instance, Original)
>>> patcher.stop()
测试
我已经编写了五个测试,每个测试首先尝试重现三个解决方案中的每一个,其次对目标代码进行实际测试。典型的模式是 assert isinstance
然后调用 is_myclass
.
所有测试都失败了。
测试 1
这是文档中提供的用于 spec
的示例的接近副本。它
与文档的不同之处在于使用 spec=<class>
而不是 spec=<instance>
。它通过
本地断言测试,但对 is_myclass
的调用失败,因为 MyClass
未被模拟。
这等同于 Michele d’Amico 对 isinstance and Mocking 中类似问题的回答。
测试 2
这是测试 1 的补丁等效项。spec
参数无法设置模拟 MyClass 的 __class__
并且测试未通过本地 assert isinstance
.
测试 3
这是文档中为使用 __class__
提供的示例的精确副本。它通过
本地断言测试,但对 is_myclass
的调用失败,因为 MyClass
未被模拟。
测试 4
这是测试 3 的补丁等效项。对 __class__
的赋值确实设置了模拟 MyClass
的 __class__
但这并没有改变它的类型,因此测试失败本地 assert isinstance
.
测试 5
这是调用补丁时使用 spec
的接近副本。它通过了本地断言测试,但只是通过访问 MyClass 的本地副本。由于此局部变量未在 is_myclass
中使用,因此调用失败。
代码
此代码是作为独立测试模块编写的,旨在 运行 PyCharm IDE。其他测试环境可能需要修改为运行
模块temp2.py
import unittest
import unittest.mock as mock
class WrongCodeTested(Exception):
pass
class MyClass:
def __init__(self):
"""This is a simplified version of a production class which must be mocked for unittesting."""
raise WrongCodeTested('Testing code in MyClass.__init__')
def is_myclass(obj):
"""This absurd stub is a simplified version of the production code."""
isinstance(obj, MyClass)
MyClass()
class ExamplesFromDocs(unittest.TestCase):
def test_1_spec(self):
obj = mock.Mock(spec=MyClass)
print(type(MyClass)) # <class 'type'>
assert isinstance(obj, MyClass) # Local assert test passes
is_myclass(obj) # Fail: MyClass instantiated
def test_2_spec_patch(self):
with mock.patch('temp2.MyClass', spec=True) as mock_myclass:
obj = mock_myclass()
print(type(mock_myclass)) # <class 'unittest.mock.MagicMock'>
print(type(MyClass)) # <class 'unittest.mock.MagicMock'>
assert isinstance(obj, MyClass) # Local assert test fails
def test_3__class__(self):
obj = mock.Mock()
obj.__class__ = MyClass
print(type(MyClass)) # <class 'type'>
isinstance(obj, MyClass) # Local assert test passes
is_myclass(obj) # Fail: MyClass instantiated
def test_4__class__patch(self):
Original = MyClass
with mock.patch('temp2.MyClass') as mock_myclass:
mock_myclass.__class__ = Original
obj = mock_myclass()
obj.__class__ = Original
print(MyClass.__class__) # <class 'temp2.MyClass'>
print(type(MyClass)) # <class 'unittest.mock.MagicMock'>
assert isinstance(obj, MyClass) # Local assert test fails
def test_5_patch_with_spec(self):
Original = MyClass
p = mock.patch('temp2.MyClass', spec=True)
MockMyClass = p.start()
obj = MockMyClass()
print(type(Original)) # <class 'type'>
print(type(MyClass)) # <class 'unittest.mock.MagicMock'>
print(type(MockMyClass)) # <class 'unittest.mock.MagicMock'>
assert isinstance(obj, Original) # Local assert test passes
is_myclass(obj) # Fail: Bad type for MyClass
你不能模拟 isinstance()
的第二个参数,不。您发现的文档涉及在 first 参数通过测试时进行模拟。如果你想产生一些可以接受的东西作为 isinstance()
的第二个参数,你实际上必须有一个 type,而不是一个实例(模拟总是实例)。
您可以使用 subclass 而不是 MyClass
,这肯定会通过,并且给它一个 __new__
方法可以让您在尝试时更改返回的内容调用它来创建一个实例:
class MockedSubClass(MyClass):
def __new__(cls, *args, **kwargs):
return mock.Mock(spec=cls) # produce a mocked instance when called
并在以下位置进行修补:
mock.patch('temp2.MyClass', new=MockedSubClass)
并使用 class 的实例作为模拟:
instance = mock.Mock(spec=MockedSubClass)
或者,这 更简单,只需使用 Mock
作为 class,并让 obj
成为 Mock
实例:
with mock.patch('temp2.MyClass', new=mock.Mock) as mocked_class:
is_myclass(mocked_class())
无论哪种方式,您的测试都会通过:
>>> with mock.patch('temp2.MyClass', new=MockedSubClass) as mocked_class:
... instance = mock.Mock(spec=MockedSubClass)
... assert isinstance(instance, mocked_class)
... is_myclass(instance)
...
>>> # no exceptions raised!
...
>>> with mock.patch('temp2.MyClass', new=mock.Mock) as mocked_class:
... is_myclass(mocked_class())
...
>>> # no exceptions raised!
...
对于您的特定测试,以下是它们失败的原因:
- 你从来没有嘲笑过
MyClass
,它仍然引用了原来的class。第一行is_myclass()
就成功了,但是第二行用的是原来的MyClass
,被坑了
MyClass
替换为 mock.Mock
实例,而不是实际类型,因此 isinstance()
引发 TypeError: isinstance() arg 2 must be a type or tuple of types
异常。
- 以与 1 完全相同的方式失败,
MyClass
留下 in-tact 并且被诱杀。
- 失败方式与 2 相同。
__class__
是一个仅对 个实例 有用的属性。 class 对象不使用 __class__
属性,您仍然有一个实例而不是 class 并且 isinstance()
引发类型错误。
- 本质上与 4 完全相同,只是您手动启动了修补程序而不是让上下文管理器处理它,并且您使用
isinstance(obj, Original)
检查实例,所以您永远不会遇到类型错误那里。类型错误改为在 is_myclass()
. 中触发
@Martijn Pieters 有一个很好的答案,我只是想补充一下我是如何用装饰器完成这个的:
import temp2
class MockedMyClass:
pass
class MockedMySubClass(MockedMyClass):
pass
@patch("temp2.MyClass", new=MockedMyClass)
def test_is_subclass(self):
assert issubclass(MockedMySubClass, temp2.MyClass)
注意:即使使用装饰器,测试也不需要任何额外的参数。
我想测试功能is_myclass
。请帮助我了解如何编写成功的测试。
def is_myclass(obj):
"""This absurd stub is a simplified version of the production code."""
isinstance(obj, MyClass)
MyClass()
文档
unittest.mock 的 Python 文档说明了解决 isinstance
问题的三种方法:
- 将
spec
参数设置为真正的class。 - 将真实的 class 分配给
__class__
属性。 - 在真实class的补丁中使用
spec
。
__class__
Normally the
__class__
attribute of an object will return its type. For a mock object with a spec,__class__
returns the spec class instead. This allows mock objects to pass isinstance() tests for the object they are replacing / masquerading as:>>> mock = Mock(spec=3) >>> isinstance(mock, int) True
__class__
is assignable to, this allows a mock to pass anisinstance()
check without forcing you to use a spec:>>> mock = Mock() >>> mock.__class__ = dict >>> isinstance(mock, dict) True
[...]
If you use
spec
orspec_set
andpatch()
is replacing a class, then the return value of the created mock will have the same spec.>>> Original = Class >>> patcher = patch('__main__.Class', spec=True) >>> MockClass = patcher.start() >>> instance = MockClass() >>> assert isinstance(instance, Original) >>> patcher.stop()
测试
我已经编写了五个测试,每个测试首先尝试重现三个解决方案中的每一个,其次对目标代码进行实际测试。典型的模式是 assert isinstance
然后调用 is_myclass
.
所有测试都失败了。
测试 1
这是文档中提供的用于 spec
的示例的接近副本。它
与文档的不同之处在于使用 spec=<class>
而不是 spec=<instance>
。它通过
本地断言测试,但对 is_myclass
的调用失败,因为 MyClass
未被模拟。
这等同于 Michele d’Amico 对 isinstance and Mocking 中类似问题的回答。
测试 2
这是测试 1 的补丁等效项。spec
参数无法设置模拟 MyClass 的 __class__
并且测试未通过本地 assert isinstance
.
测试 3
这是文档中为使用 __class__
提供的示例的精确副本。它通过
本地断言测试,但对 is_myclass
的调用失败,因为 MyClass
未被模拟。
测试 4
这是测试 3 的补丁等效项。对 __class__
的赋值确实设置了模拟 MyClass
的 __class__
但这并没有改变它的类型,因此测试失败本地 assert isinstance
.
测试 5
这是调用补丁时使用 spec
的接近副本。它通过了本地断言测试,但只是通过访问 MyClass 的本地副本。由于此局部变量未在 is_myclass
中使用,因此调用失败。
代码
此代码是作为独立测试模块编写的,旨在 运行 PyCharm IDE。其他测试环境可能需要修改为运行
模块temp2.py
import unittest
import unittest.mock as mock
class WrongCodeTested(Exception):
pass
class MyClass:
def __init__(self):
"""This is a simplified version of a production class which must be mocked for unittesting."""
raise WrongCodeTested('Testing code in MyClass.__init__')
def is_myclass(obj):
"""This absurd stub is a simplified version of the production code."""
isinstance(obj, MyClass)
MyClass()
class ExamplesFromDocs(unittest.TestCase):
def test_1_spec(self):
obj = mock.Mock(spec=MyClass)
print(type(MyClass)) # <class 'type'>
assert isinstance(obj, MyClass) # Local assert test passes
is_myclass(obj) # Fail: MyClass instantiated
def test_2_spec_patch(self):
with mock.patch('temp2.MyClass', spec=True) as mock_myclass:
obj = mock_myclass()
print(type(mock_myclass)) # <class 'unittest.mock.MagicMock'>
print(type(MyClass)) # <class 'unittest.mock.MagicMock'>
assert isinstance(obj, MyClass) # Local assert test fails
def test_3__class__(self):
obj = mock.Mock()
obj.__class__ = MyClass
print(type(MyClass)) # <class 'type'>
isinstance(obj, MyClass) # Local assert test passes
is_myclass(obj) # Fail: MyClass instantiated
def test_4__class__patch(self):
Original = MyClass
with mock.patch('temp2.MyClass') as mock_myclass:
mock_myclass.__class__ = Original
obj = mock_myclass()
obj.__class__ = Original
print(MyClass.__class__) # <class 'temp2.MyClass'>
print(type(MyClass)) # <class 'unittest.mock.MagicMock'>
assert isinstance(obj, MyClass) # Local assert test fails
def test_5_patch_with_spec(self):
Original = MyClass
p = mock.patch('temp2.MyClass', spec=True)
MockMyClass = p.start()
obj = MockMyClass()
print(type(Original)) # <class 'type'>
print(type(MyClass)) # <class 'unittest.mock.MagicMock'>
print(type(MockMyClass)) # <class 'unittest.mock.MagicMock'>
assert isinstance(obj, Original) # Local assert test passes
is_myclass(obj) # Fail: Bad type for MyClass
你不能模拟 isinstance()
的第二个参数,不。您发现的文档涉及在 first 参数通过测试时进行模拟。如果你想产生一些可以接受的东西作为 isinstance()
的第二个参数,你实际上必须有一个 type,而不是一个实例(模拟总是实例)。
您可以使用 subclass 而不是 MyClass
,这肯定会通过,并且给它一个 __new__
方法可以让您在尝试时更改返回的内容调用它来创建一个实例:
class MockedSubClass(MyClass):
def __new__(cls, *args, **kwargs):
return mock.Mock(spec=cls) # produce a mocked instance when called
并在以下位置进行修补:
mock.patch('temp2.MyClass', new=MockedSubClass)
并使用 class 的实例作为模拟:
instance = mock.Mock(spec=MockedSubClass)
或者,这 更简单,只需使用 Mock
作为 class,并让 obj
成为 Mock
实例:
with mock.patch('temp2.MyClass', new=mock.Mock) as mocked_class:
is_myclass(mocked_class())
无论哪种方式,您的测试都会通过:
>>> with mock.patch('temp2.MyClass', new=MockedSubClass) as mocked_class:
... instance = mock.Mock(spec=MockedSubClass)
... assert isinstance(instance, mocked_class)
... is_myclass(instance)
...
>>> # no exceptions raised!
...
>>> with mock.patch('temp2.MyClass', new=mock.Mock) as mocked_class:
... is_myclass(mocked_class())
...
>>> # no exceptions raised!
...
对于您的特定测试,以下是它们失败的原因:
- 你从来没有嘲笑过
MyClass
,它仍然引用了原来的class。第一行is_myclass()
就成功了,但是第二行用的是原来的MyClass
,被坑了 MyClass
替换为mock.Mock
实例,而不是实际类型,因此isinstance()
引发TypeError: isinstance() arg 2 must be a type or tuple of types
异常。- 以与 1 完全相同的方式失败,
MyClass
留下 in-tact 并且被诱杀。 - 失败方式与 2 相同。
__class__
是一个仅对 个实例 有用的属性。 class 对象不使用__class__
属性,您仍然有一个实例而不是 class 并且isinstance()
引发类型错误。 - 本质上与 4 完全相同,只是您手动启动了修补程序而不是让上下文管理器处理它,并且您使用
isinstance(obj, Original)
检查实例,所以您永远不会遇到类型错误那里。类型错误改为在is_myclass()
. 中触发
@Martijn Pieters 有一个很好的答案,我只是想补充一下我是如何用装饰器完成这个的:
import temp2
class MockedMyClass:
pass
class MockedMySubClass(MockedMyClass):
pass
@patch("temp2.MyClass", new=MockedMyClass)
def test_is_subclass(self):
assert issubclass(MockedMySubClass, temp2.MyClass)
注意:即使使用装饰器,测试也不需要任何额外的参数。