【问题标题】:How to mock an attribute variable that returns a class instance如何模拟返回类实例的属性变量
【发布时间】:2021-11-16 07:48:56
【问题描述】:

所以我有这个类位于文件夹/层/base.py 里面有这样的东西:

from folder.plugin import load_plugin

class BaseLayer:

  def __init__(self):
    self.tileindex = load_plugin()

我需要为该类中已经存在的函数添加单元测试。我的问题是,函数load_plugin() 返回位于文件夹/tileindex/base.py 中的类的实例。正因为如此,它在多个不同的函数中多次发生,一行看起来像这样:

def somefunction(self):
  key = self.tileindex.get_key(...)
  r = self.tileindex.bulk_add(...)
  self.tileindex.add(...)

而且我不知道如何模拟它。起初我在嘲笑load_plugin 并返回任何值,以便之后我可以断言它。但现在我已经看到这些函数使用self.tileindex 作为另一个类的实例,我不知道该怎么办。例如:

def register(self):
        """
        Registers a file into the system
        :returns: `bool` of status result
        """
        items = [item for item in self.items if item['register_status']]
        if len(items) > 1:
            item_bulk = []
            for item in items:
                item_bulk.append(self.layer2dict(item))
            LOGGER.debug('Adding to tileindex (bulk)')
            r = self.tileindex.bulk_add(item_bulk)
            status = r[items[0]['identifier']]

当我嘲笑load_plugin 时,代码在最后一行显示TypeError: 'Mock' object is not subscriptable 失败。

我尝试导入实例化的类并直接模拟它。但是由于某种原因,我一输入@patch('folder.tileindex.base')就会收到错误AttributeError: <Group tileindex> does not have the attribute 'base'

有什么方法可以模拟 self.tileindex 本身,以便测试其余代码吗?

谢谢!

【问题讨论】:

    标签: python python-3.x unit-testing mocking python-unittest


    【解决方案1】:

    确保不要使用unittest.mock.Mock,而是使用unittest.mock.MagicMock,原因是here。你可以关注这个关于Mocking Classes的文档(所有这些都将使用MagicMock)。

    对于您的情况,这里有 3 个选项来模拟 load_plugin() 返回的对象。您可以选择最适合您的需求。

    • mock_plugin_return_values - 通过 return_value 模拟
    • mock_plugin_side_effect - 通过 side_effect 模拟
    • mock_plugin_stub - 通过存根类进行模拟

    文件树

    .
    ├── folder
    │   ├── layer
    │   │   └── base.py
    │   ├── plugin.py
    │   └── tileindex
    │       └── base.py
    └── tests
        └── test_layer.py
    

    文件夹/层/base.py

    from folder.plugin import load_plugin
    
    class BaseLayer:
    
        def __init__(self):
            self.tileindex = load_plugin()
    
        def somefunction(self):
            a = self.tileindex.add("a")
            print("add:", a)
    
            key = self.tileindex.get_key("a")
            print("get_key:", key)
    
            r = self.tileindex.bulk_add([1, 2, 3])
            print("bulk_add:", r)
    
            status = r['identifier']
            print("status:", status)
    
            return a, key, r, status
    

    文件夹/plugin.py

    from folder.tileindex.base import SomePlugin
    
    
    def load_plugin():
        return SomePlugin()
    

    文件夹/tileindex/base.py

    class SomePlugin():
        pass
    

    test/test_layer.py

    import pytest
    
    from folder.layer.base import BaseLayer
    
    
    # Note, this requires <pip install pytest-mock>
    
    
    @pytest.fixture
    def mock_plugin_return_values(mocker):
        mock_cls = mocker.patch("folder.plugin.SomePlugin")
        mock_obj = mock_cls.return_value
    
        mock_obj.add.return_value = "Anything!"
        mock_obj.get_key.return_value = "Something!"
        mock_obj.bulk_add.return_value = {"identifier": "Nothing!"}
    
    
    @pytest.fixture
    def mock_plugin_side_effect(mocker):
        mock_cls = mocker.patch("folder.plugin.SomePlugin")
        mock_obj = mock_cls.return_value
    
        mock_obj.add.side_effect = lambda arg: f"Adding {arg} here"
        mock_obj.get_key.side_effect = lambda arg: f"Getting {arg} now"
        mock_obj.bulk_add.side_effect = lambda arg: {"identifier": f"Adding the {len(arg)} elements"}
    
    @pytest.fixture
    def mock_plugin_stub(mocker):
        # Option 1: Create a new class
        # class SomePluginStub:
    
        # Option 2: Inehrit from the actual class and just override the functions to mock
        from folder.tileindex.base import SomePlugin
        class SomePluginStub(SomePlugin):
    
            def add(self, arg):
                return f"Adding {arg} here"
    
            def get_key(self, arg):
                return f"Getting {arg} now"
    
            def bulk_add(self, arg):
                return {"identifier": f"Adding the {len(arg)} elements"}
    
        mocker.patch("folder.plugin.SomePlugin", SomePluginStub)
    
    
    def test_return_values(mock_plugin_return_values):
        layer = BaseLayer()
        result = layer.somefunction()
        print(result)
        assert result == ('Anything!', 'Something!', {'identifier': 'Nothing!'}, 'Nothing!')
    
    
    def test_side_effect(mock_plugin_side_effect):
        layer = BaseLayer()
        result = layer.somefunction()
        print(result)
        assert result == ('Adding a here', 'Getting a now', {'identifier': 'Adding the 3 elements'}, 'Adding the 3 elements')
    
    
    def test_stub(mock_plugin_stub):
        layer = BaseLayer()
        result = layer.somefunction()
        print(result)
        assert result == ('Adding a here', 'Getting a now', {'identifier': 'Adding the 3 elements'}, 'Adding the 3 elements')
    

    输出

    $ pytest -q -rP
    ...                                                                                     [100%]
    =========================================== PASSES ============================================
    _____________________________________ test_return_values ______________________________________
    ------------------------------------ Captured stdout call -------------------------------------
    add: Anything!
    get_key: Something!
    bulk_add: {'identifier': 'Nothing!'}
    status: Nothing!
    ('Anything!', 'Something!', {'identifier': 'Nothing!'}, 'Nothing!')
    ______________________________________ test_side_effect _______________________________________
    ------------------------------------ Captured stdout call -------------------------------------
    add: Adding a here
    get_key: Getting a now
    bulk_add: {'identifier': 'Adding the 3 elements'}
    status: Adding the 3 elements
    ('Adding a here', 'Getting a now', {'identifier': 'Adding the 3 elements'}, 'Adding the 3 elements')
    __________________________________________ test_stub __________________________________________
    ------------------------------------ Captured stdout call -------------------------------------
    add: Adding a here
    get_key: Getting a now
    bulk_add: {'identifier': 'Adding the 3 elements'}
    status: Adding the 3 elements
    ('Adding a here', 'Getting a now', {'identifier': 'Adding the 3 elements'}, 'Adding the 3 elements')
    3 passed in 0.06s
    

    【讨论】:

    • 感谢您的回答,非常详细,我很感激。只有一件事是 SomePlugin 实际上来自另一个文件夹。在load_plugin 函数中有一个module = importlib.import_module(packagename),然后它将返回带有插件定义的类。所以我尝试做的是@patch(folder.tileindex.base),以便我可以使用它,但我总是收到错误AttributeError: &lt;Group tileindex&gt; does not have the attribute 'base'。导入本身from folder.tileindex.base import module 有效,但补丁总是失败...
    • 为了不进一步处理SomePlugin 的所有复杂性,您只需将mock_cls = mocker.patch("folder.plugin.SomePlugin") 行更改为mock_cls = mocker.patch("folder.layer.base.load_plugin"),这样我们就可以直接修补load_plugin() 而不是它实例化的类.但这仅适用于mock_plugin_return_valuesmock_plugin_side_effect 而不适用于mock_plugin_stub(尽管考虑到这些事实,我认为这无论如何都不是首选)。可以试试吗?
    • 这就是我正在做的事情,但失败了。执行以下操作时:r = self.tileindex.bulk_add(item_bulk)status = r[items[0]['identifier']] 无法说出TypeError: 'Mock' object is not subscriptable
    • 您是否手动将任何内容初始化为unittest.mock.Mock 对象,您将其与我的答案一起包含在内?还是您使用的是我在回答中已经提供的夹具?
    • uhm .. 为什么现在测试通过了,因为我已经更改了 unittest.mock.Mock 只是一个补丁...我认为它们是等效的,我想有很大的不同。我在做mocked_load_plugin = Mock(),然后用@patch('folder.layer.base.load_plugin', new=mocked_load_plugin) 修补我的整个测试类,这样我就不必将它作为参数传递给每个测试函数。现在我尝试将它传递给每个函数并删除 new 并且测试通过了......我有点生气,不会撒谎,但非常感谢你的帮助!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-07-17
    • 1970-01-01
    • 1970-01-01
    • 2019-04-17
    • 1970-01-01
    • 2015-12-08
    • 2016-04-27
    相关资源
    最近更新 更多