关于python:如何模拟修补isinstance测试中使用的类?

How can I mock patch a class used in an isinstance test?

我想测试函数is_myclass。请帮助我理解如何编写一个成功的测试。

1
2
3
4
def is_myclass(obj):
   """This absurd stub is a simplified version of the production code."""
    isinstance(obj, MyClass)
    MyClass()

文档

unittest.mock的python文档说明了解决EDOCX1问题的三种方法:

    百万千克1将spec参数设置为实数类。百万千克1百万千克1将实数类赋给__class__属性。百万千克1百万千克1在真实类的补丁中使用spec。百万千克1

__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:

1
2
3
>>> 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:

1
2
3
4
>>> 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.

1
2
3
4
5
6
>>> 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=而不是spec=。它通过了本地断言测试,但对is_myclass的调用失败,因为MyClass没有被模拟。

这相当于米歇尔达米科对类似问题的回答是站在立场和嘲弄。

测试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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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


你不能模仿EDOCX1的第二个参数(0),不。当第一个参数通过测试时,你发现的文档担心会进行模仿。如果您想要生成一个可接受的东西作为isinstance()的第二个参数,那么实际上您必须有一个类型,而不是实例(而mock总是实例)。

您可以使用一个子类来代替MyClass,这肯定是可以通过的,如果给它一个__new__方法,您可以在尝试调用它来创建实例时更改返回的内容:

1
2
3
class MockedSubClass(MyClass):
    def __new__(cls, *args, **kwargs):
        return mock.Mock(spec=cls)  # produce a mocked instance when called

把它补上:

1
mock.patch('temp2.MyClass', new=MockedSubClass)

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

1
instance = mock.Mock(spec=MockedSubClass)

或者,这就简单多了,只使用Mock作为类,并让obj作为Mock的实例:

1
2
with mock.patch('temp2.MyClass', new=mock.Mock) as mocked_class:
    is_myclass(mocked_class())

不管怎样,您的测试都会通过:

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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,它仍然引用原始类。is_myclass()的第一行成功,但第二行使用的是原来的MyClass,是陷阱。
  • MyClass替换为mock.Mock实例,而不是实际类型,因此isinstance()引发TypeError: isinstance() arg 2 must be a type or tuple of types异常。
  • 失败的方式与1失败的方式完全相同,MyClass保持机智,被诱捕。
  • 失败的方式与2相同。__class__是一个只对实例有用的属性。类对象不使用__class__属性,您仍然有一个实例而不是类,isinstance()会引发类型错误。
  • 基本上与4完全相同,只是您手动启动了补丁程序,而不是让上下文管理器来处理它,并且您使用isinstance(obj, Original)来检查实例,这样就不会出现类型错误。类型错误在is_myclass()中触发。