Python3 模拟:assert_has_calls 用于生产代码方法?

Python3 mock: assert_has_calls for production-code methods?

我有这个作品class:

class MyClass:
    def __init__(self):
        self.value = None

    def set_value(self, value):
        self.value = value

    def foo(self):
        # work with self.value here
        # raise RuntimeError("error!")
        return "a"

正在从另一个地方使用,像这样:

class Caller:
    def bar(self, smth):
        obj = MyClass()
        obj.set_value(smth)
        # ...
        # try:
        obj.foo()
        # except MyError:
        #     pass

        obj.set_value("str2")
        # obj.foo()

我得到了这个:

class MyError(Exception):
    pass

在我的测试中,我想确保 Caller.bar 调用 obj.set_value,首先使用 smth="a",然后使用 smth="b",但我想要它真正设置值(即调用真正的 set_value 方法)。有什么办法让我告诉模拟使用实际方法,以便我稍后可以阅读它调用的内容吗?

P.S。我知道我可以更改 "foo" 以要求参数 "smth" 这样我就可以摆脱 "set_value",但我想知道是否还有其他选择。


好的,所以我在测试中尝试了这个:

def test_caller(self):
     with patch('fullpath.to.MyClass', autospec=MyClass) as mock:
         mock.foo.side_effect = [MyError("msg"), "text"]

         caller = Caller()
         caller.bar("str1")

         calls = [call("str1"), call("str2")]
         mock.set_value.assert_has_calls(calls)

但是我看到模拟没有成功,因为真正的 "foo" 在我希望它首先引发 MyError 时被调用,然后是 return "text".

此外,断言失败:

AssertionError: Calls not found.
Expected: [call('str1'), call('str2')]
Actual: []

这里的问题是您模拟了 Class,并且没有正确使用 class 的 实例 。这就是为什么事情没有按预期运行的原因。

所以,让我们来看看发生了什么。

就在这里:

with patch('fullpath.to.MyClass', autospec=MyClass) as mock:

所以,您在这里所做的只是模拟您的 class MyClass。所以,当你这样做时:

mock.set_value.assert_has_calls(calls)

并检查执行单元测试时发生了什么,您的 mock 调用实际上将包含以下内容:

[call().set_value('str1'), call().foo(), call().set_value('str2')]

注意call 写成call()call 是参考你的 mock 这里。因此,考虑到这一点,您需要使用 called(在模拟世界的上下文中又名 return_valuemock 来正确引用您的模拟对象正在尝试测试。解决这个问题的快速方法是简单地使用 mock()。所以你只需要更改为:

mock().set_value.assert_has_calls(calls)

但是,为了更明确地说明您在做什么,您可以声明您实际上正在使用调用 mock 的结果。此外,实际上最好注意使用更明确的名称,而不是 mock。尝试 MyClassMock,这又将您的实例命名为 my_class_mock_obj

my_class_mock_obj = MyClassMock.return_value

因此,在您的单元测试中,更明确的是您正在使用 class 的模拟对象。此外,最好在进行方法调用之前设置所有模拟,并且对于 foo.side_effect 确保您也在使用 instance 模拟对象。根据您最近对异常处理的更新,请保留您的 try/except 不加评论。将所有这些放在一起,您有:

def test_caller(self):
    with patch('tests.test_dummy.MyClass', autospec=MyClass) as MyClassMock:
        my_class_mock_obj = MyClassMock.return_value
        my_class_mock_obj.foo.side_effect = [MyError("msg"), "text"]

        caller = Caller()
        caller.bar("str1")

        calls = [call("str1"), call("str2")]

        my_class_mock_obj.set_value.assert_has_calls(calls)