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

lem*_*sss 5 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()
Run Code Online (Sandbox Code Playgroud)

文档

unittest.mock 的 Python 文档说明了解决该isinstance问题的三种方法:

  • spec参数设置为真实的类。
  • 将真正的类分配给__class__属性。
  • spec在真实类的补丁中使用。

__class__

通常,__class__对象的属性将返回其类型。对于具有规范的模拟对象,__class__返回规范类。这允许模拟对象通过 isinstance() 测试它们正在替换/伪装为的对象:

>>> mock = Mock(spec=3)
>>> isinstance(mock, int)
True
Run Code Online (Sandbox Code Playgroud)

__class__可分配给,这允许模拟通过isinstance()检查而不强迫您使用规范:

>>> mock = Mock()
>>> mock.__class__ = dict
>>> isinstance(mock, dict)
True
Run Code Online (Sandbox Code Playgroud)

[...]

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

>>> Original = Class
>>> patcher = patch('__main__.Class', spec=True)
>>> MockClass = patcher.start()
>>> instance = MockClass()
>>> assert isinstance(instance, Original)
>>> patcher.stop()
Run Code Online (Sandbox Code Playgroud)

测试

我编写了五个测试,每个测试首先尝试重现三个解决方案中的每一个,其次对目标代码进行实际测试。典型模式 assert isinstance 后跟调用is_myclass.

所有测试都失败。

测试 1

这是文档中提供的用于使用spec. 它与文档的不同之处在于使用spec=<class>而不是spec=<instance>. 它通过了本地断言测试,但调用is_myclass失败,因为MyClass没有被模拟。

这相当于 Michele d'Amico 在isinstance 和 Mocking 中对类似问题的回答。

测试 2

这是测试 1 的补丁等价物。spec参数无法设置模拟__class__MyClass 的 并且测试失败本地assert isinstance.

测试 3

这是文档中提供的用于使用__class__. 它通过了本地断言测试,但调用is_myclass失败,因为MyClass没有被模拟。

测试 4

这是测试 3 的补丁等价物。 对的赋值__class__确实设置了模拟的__class__MyClass但这不会改变它的类型,因此测试失败了本地assert isinstance.

测试 5

这是spec在调用 patch 中使用的密切副本。它通过了本地断言测试,但只能通过访问 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
Run Code Online (Sandbox Code Playgroud)

Mar*_*ers 6

你不能嘲笑isinstance(), no的第二个参数。您发现的文档涉及在第一个参数通过测试时进行模拟。如果你想产生一些可以作为 的第二个参数的东西isinstance(),你实际上必须有一个type,而不是一个实例(并且模拟总是实例)。

您可以使用子类而MyClass不是,它肯定会通过,并且给它一个__new__方法可以让您在尝试调用它以创建实例时更改返回的内容:

class MockedSubClass(MyClass):
    def __new__(cls, *args, **kwargs):
        return mock.Mock(spec=cls)  # produce a mocked instance when called
Run Code Online (Sandbox Code Playgroud)

并修补:

mock.patch('temp2.MyClass', new=MockedSubClass)
Run Code Online (Sandbox Code Playgroud)

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

instance = mock.Mock(spec=MockedSubClass)
Run Code Online (Sandbox Code Playgroud)

或者,这更简单,只需Mock用作类,并且obj是一个Mock实例:

with mock.patch('temp2.MyClass', new=mock.Mock) as mocked_class:
    is_myclass(mocked_class())
Run Code Online (Sandbox Code Playgroud)

无论哪种方式,您的测试都会通过:

>>> 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!
...
Run Code Online (Sandbox Code Playgroud)

对于您的特定测试,这就是它们失败的原因:

  1. 你从来没有嘲笑过MyClass,它仍然引用原始类。第一行is_myclass()成功,但第二行使用原来的MyClass,被诱杀了。
  2. MyClass被替换为mock.Mock实例,而不是实际类型,因此isinstance()引发TypeError: isinstance() arg 2 must be a type or tuple of types异常。
  3. 失败的方式与 1 失败完全相同,保持原样MyClass并被诱杀。
  4. 失败的方式与 2 相同。__class__是一个仅对实例有用的属性。类对象不使用该__class__属性,您仍然有一个实例而不是类并isinstance()引发类型错误。
  5. 本质上与 4 完全相同,只是您手动启动了修补程序而不是让上下文管理器来处理它,并且您曾经isinstance(obj, Original)检查过实例,因此您永远不会在那里遇到类型错误。类型错误是在is_myclass().