检查多个模拟中的呼叫顺序

时间:2014-03-27 02:42:39

标签: python function mocking python-mock

我有三个功能,我试图测试。

的呼叫顺序

让我们说模块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 个答案:

答案 0 :(得分:25)

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

Tracking order of calls and less verbose call assertions

上查看更多示例

希望有所帮助。

答案 1 :(得分:3)

我今天需要这个答案,但是问题中的示例代码确实很难阅读,因为调用args与管理器上的模拟名相同。测试。 Here's the official documentation on this concept,以下是非机器人的更清晰示例。为了示例,我正在修补的所有模块都是这样构成的:

@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创建的名称)的副本,此代码才能起作用。如果您创建自己的attach_mock对象而没有名称,则不必使用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

答案 2 :(得分:-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)

在测试文件中,您创建一个模拟包装器类,然后在调用 self 时将模拟包装器作为参数插入 Wrapper.main_method(注意这不会实例化该类)。

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