检查多个模拟对象的调用顺序

46 投票
3 回答
11901 浏览
提问于 2025-04-18 00:05

我有三个函数,想要测试它们的调用顺序。

假设在一个叫做 module.py 的模块里,我有以下内容:

# module.py    

def a(*args):
    # do the first thing

def b(*args):
    # do a second thing

def c(*args):
    # do a third thing


def main_routine():
    a_args = ('a')
    b_args = ('b')
    c_args = ('c')

    a(*a_args)
    b(*b_args)
    c(*c_args)

我想检查 b 是在 a 之后被调用的,并且在 c 之前被调用。所以为 a、b 和 c 各自创建一个模拟对象是很简单的:

# tests.py

@mock.patch('module.a')
@mock.patch('module.b')
@mock.patch('module.c')
def test_main_routine(c_mock, b_mock, a_mock):
    # test all the things here

检查每个模拟对象是否被调用也很简单。那么,怎么检查它们之间的调用顺序呢?

call_args_list 这个方法不行,因为它是为每个模拟对象单独维护的。

我尝试使用一个副作用来记录每次调用:

calls = []
def register_call(*args):
    calls.append(mock.call(*args))
    return mock.DEFAULT

a_mock.side_effect = register_call
b_mock.side_effect = register_call
c_mock.side_effect = register_call

但这样只能让我看到调用时传入的参数,而不能知道实际是哪个模拟对象被调用了。我可以加一点逻辑:

# tests.py
from functools import partial

def register_call(*args, **kwargs):
    calls.append(kwargs.pop('caller', None), mock.call(*args, **kwargs))
    return mock.DEFAULT

a_mock.side_effect = partial(register_call, caller='a')
b_mock.side_effect = partial(register_call, caller='b')
c_mock.side_effect = partial(register_call, caller='c')

这样似乎可以解决问题……不过有没有更好的方法呢?感觉应该有现成的 API 能做到这一点,而我却没找到。

3 个回答

-1

一个更简洁的解决方案是把你的函数放进一个类里,然后在测试中模拟这个类。这样就不需要进行任何补丁处理(这总是个好事)。

# module.py

class Wrapper:
    def a(self, *args):
        pass

    def b(self, *args):
        pass

    def c(self, *args):
        pass

    def main_routine(self):
        a_args = ('arg for a',)
        b_args = ('arg for b',)
        c_args = ('arg for c',)

        self.a(*a_args)
        self.b(*b_args)
        self.c(*c_args)

在测试文件中,你创建一个模拟的包装类,然后在调用 Wrapper.main_method 时,把这个模拟的包装类作为参数 self 传进去(注意,这样并不会实例化这个类)。

# module_test.py

from unittest.mock import MagicMock, call

from module import Wrapper


def test_main_routine():
    mock_wrapper = MagicMock()
    Wrapper.main_routine(mock_wrapper)
    expected_calls = [call.a('arg for a'),
                      call.b('arg for b'),
                      call.c('arg for c')]
    mock_wrapper.assert_has_calls(expected_calls)

好处:

  • 不需要补丁处理
  • 在测试中,你只需要输入一次被调用的方法名(而不是2-3次)
  • 使用 assert_has_calls,而不是把 mock_calls 属性和调用列表进行比较。
  • 可以做成一个通用的 check_for_calls 函数(见下文)
# module_better_test.py

from unittest.mock import MagicMock, call

from module import Wrapper


def test_main_routine():
    expected_calls = [call.a('arg for a'),
                      call.b('arg for b'),
                      call.c('arg for c')]
    check_for_calls('main_routine', expected_calls)


def check_for_calls(method, expected_calls):
    mock_wrapper = MagicMock()
    getattr(Wrapper, method)(mock_wrapper)
    mock_wrapper.assert_has_calls(expected_calls)

25

今天我需要这个答案,但问题中的示例代码真的很难读,因为调用的参数和管理器中的模拟对象名字是一样的,测试范围内也是如此。这里有关于这个概念的官方文档,下面是一个更清晰的例子,适合普通人理解。所有我在这里修改的模块都是为了这个例子而虚构的:

@patch('module.file_reader')
@patch('module.json_parser')
@patch('module.calculator')
def test_main_routine(mock_calculator, mock_json_parser, mock_file_reader):
    manager = Mock()

    # First argument is the mock to attach to the manager.
    # Second is the name for the field on the manager that holds the mock.
    manager.attach_mock(mock_file_reader, 'the_mock_file_reader')
    manager.attach_mock(mock_json_parser, 'the_mock_json_parser')
    manager.attach_mock(mock_calculator, 'the_mock_calculator')
    
    module.main_routine()

    expected_calls = [
        call.the_mock_file_reader('some file'),
        call.the_mock_json_parser('some json'),
        call.the_mock_calculator(1, 2)
    ]
    assert manager.mock_calls == expected_calls

注意,在这种情况下你必须使用 attach_mock,因为你的模拟对象是通过 patch 创建的。带有名字的模拟对象,包括那些通过 patch 创建的,必须通过 attach_mock 来连接,这样代码才能正常工作。如果你自己创建没有名字的 Mock 对象,就不需要使用 attach_mock

def test_main_routine(mock_calculator, mock_json_parser, mock_file_reader):
    manager = Mock()

    mock_file_reader = Mock()
    mock_json_parser = Mock()
    mock_calculator = Mock()

    manager.the_mock_file_reader = mock_file_reader
    manager.the_mock_json_parser = mock_json_parser
    manager.the_mock_calculator = mock_calculator
    
    module.main_routine()

    expected_calls = [
        call.the_mock_file_reader('some file'),
        call.the_mock_json_parser('some json'),
        call.the_mock_calculator(1, 2)
    ]
    assert manager.mock_calls == expected_calls

如果你想在顺序或预期调用缺失时得到一个清晰的断言失败信息,可以使用下面的断言行。

self.assertListEqual(manager.mock_calls, [
    call.the_mock_file_reader('some file'),
    call.the_mock_json_parser('some json'),
    call.the_mock_calculator(1, 2)
])
54

定义一个 Mock 管理器,并通过 attach_mock() 方法将模拟对象附加到它上面。然后检查 mock_calls

@patch('module.a')
@patch('module.b')
@patch('module.c')
def test_main_routine(c, b, a):
    manager = Mock()
    manager.attach_mock(a, 'a')
    manager.attach_mock(b, 'b')
    manager.attach_mock(c, 'c')

    module.main_routine()

    expected_calls = [call.a('a'), call.b('b'), call.c('c')]
    assert manager.mock_calls == expected_calls

为了测试它是否有效,可以改变 main_routine() 函数中函数调用的顺序,看看是否会抛出 AssertionError

想要查看更多示例,可以访问 跟踪调用顺序和更简洁的调用断言(链接已失效;可以替换为:https://docs.python.org/3/library/unittest.mock.html#attaching-mocks-as-attributes

希望这对你有帮助。

撰写回答