如何模拟 returns 一个 class 实例的属性变量

How to mock an attribute variable that returns a class instance

所以我有这个 class 位于 folder/layer/base.py 里面有这样的东西:

from folder.plugin import load_plugin

class BaseLayer:

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

我需要向 class 中已有的函数添加单元测试。我的问题是,函数 load_plugin() returns 是 class 的一个实例,位于 folder/tileindex/base.py 中。因此,它发生了多次,并且在多个不同的函数中,一行看起来像这样:

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

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

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

我尝试导入实例化的 class 并直接模拟它。但是由于某种原因,我一输入 @patch('folder.tileindex.base').

就收到错误 AttributeError: <Group tileindex> does not have the attribute 'base'

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

谢谢!

确保不使用 unittest.mock.Mock,而是使用 unittest.mock.MagicMock,原因如下 . You can follow this documentation about Mocking Classes(所有这些都将使用 MagicMock)。

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

  • mock_plugin_return_values - 通过 return_value
  • 进行模拟
  • mock_plugin_side_effect - 通过 side_effect
  • 进行模拟
  • mock_plugin_stub - 通过打桩 class
  • 来模拟

文件树

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

folder/layer/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

folder/plugin.py

from folder.tileindex.base import SomePlugin


def load_plugin():
    return SomePlugin()

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