如何模拟修补isinstance测试中使用的类?

时间:2018-04-08 13:36:00

标签: python unit-testing mocking python-unittest

我想测试函数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__属性。
  • 在真实班级的补丁中使用spec
  

__class__

     

通常,对象的__class__属性将返回其类型。对于带有spec的模拟对象,__class__会返回spec类。这允许模拟对象将其替换/伪装的对象的isinstance()测试传递为:

>>> mock = Mock(spec=3)
>>> isinstance(mock, int)
True
     

__class__可分配,这允许模拟传递isinstance()支票而不强制您使用规范:

>>> mock = Mock()
>>> mock.__class__ = dict
>>> isinstance(mock, dict)
True
     

[...]

     

如果您使用specspec_setpatch()正在替换某个类,那么创建的模拟的返回值将具有相同的规范。

>>> 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__的分配确实设置了模拟__class__的{​​{1}},但这并没有改变其类型,因此测试失败了本地MyClass

测试5

这是在调用补丁时使用assert isinstance的完整副本。它通过本地断言测试,但仅通过访问MyClass的本地副本。由于在spec内未使用此局部变量,因此调用失败。

代码

此代码是作为独立测试模块编写的,旨在在PyCharm IDE中运行。您可能需要将其修改为在其他测试环境中运行。

模块temp2.​​py

is_myclass

2 个答案:

答案 0 :(得分:2)

你无法模仿isinstance()的第二个参数,不能。您发现的文档涉及将作为第一个参数的模拟通过测试。如果你想生成一些可接受的东西作为isinstance()的第二个参数,你实际上必须有一个类型,而不是一个实例(而模拟总是实例)。

您可以使用子类而不是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)

并使用该类的实例作为模拟:

instance = mock.Mock(spec=MockedSubClass)

或者,这是更简单,只需使用Mock作为类,并objMock个实例:

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!
...

对于您的具体测试,这就是他们失败的原因:

  1. 你从未嘲笑MyClass,它仍然引用原始类。 is_myclass()的第一行成功,但第二行使用原始的MyClass并且它是陷阱陷阱。
  2. MyClass已替换为mock.Mock个实例,而非实际类型,因此isinstance()会引发TypeError: isinstance() arg 2 must be a type or tuple of types例外。
  3. 与失败完全相同的方式失败,MyClass保持原状并且陷入困境。
  4. 失败的方式与2相同。 __class__是仅对实例有用的属性。类对象不使用__class__属性,您仍然有实例而不是类,而isinstance()会引发类型错误。
  5. 基本上与4完全相同,只是您手动启动了修补程序而不是让上下文管理器处理它,并且您使用isinstance(obj, Original)来检查实例,因此您从未在那里得到类型错误。而是在is_myclass()
  6. 中触发了类型错误

答案 1 :(得分:0)

@Martijn Pieters有一个很好的答案,我只是想补充一下我如何用Decoraters完成此工作:

import temp2

class MockedMyClass:
    pass

class MockedMySubClass(MockedMyClass):
    pass

@patch("temp2.MyClass", new=MockedMyClass)
def test_is_subclass(self):
    assert issubclass(MockedMySubClass, temp2.MyClass)

注意::即使使用了修饰符,该测试也不需要任何其他参数。