【问题标题】:How can I mock patch a class used in an isinstance test?如何模拟在 isinstance 测试中使用的类?
【发布时间】:2018-09-17 23:53:11
【问题描述】:

我想测试函数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__ 属性将返回其类型。对于具有规范的模拟对象,__class__ 返回规范类。这允许模拟对象通过 isinstance() 测试它们正在替换/伪装为的对象:

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

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

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

[...]

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

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

【问题讨论】:

    标签: python unit-testing mocking python-unittest


    【解决方案1】:

    你不能模拟isinstance() 的第二个参数,不。您发现的文档涉及将模拟作为 first 参数通过测试。如果你想生成可以作为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 作为类,并让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!
    ...
    

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

    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() 中触发类型错误。

    【讨论】:

    • 值得一提的是,您的更简单的解决方案不适用于我的生产代码,因为我需要在上下文管理器范围之外访问“mockedclass()”。
    • 还应该注意的是,每次调用模拟类()都会创建新的模拟对象,不会比较相等。如果需要相等,则必须将 MockedSubClass 实现为单例。这将是在测试代码中实例化对象的情况,并且需要通过将其与单独创建的测试对象进行比较来确保它已被正确实例化。
    • @lemi57ssss 是的,现在__new__ 实现是使您的问题中的示例正常工作所需的最简单版本。如果有其他要求,请调整调用类产生的“实例”以满足这些要求。
    【解决方案2】:

    @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)
    

    注意:即使使用了装饰器,测试也不需要任何额外的参数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-05
      • 1970-01-01
      • 2023-03-05
      • 2020-10-14
      • 2019-02-08
      相关资源
      最近更新 更多