我正在尝试测试三个函数的调用顺序。
假设在模块 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 中应该已经有一些东西可以做到这一点,而我却缺少它。
定义一个
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()
函数 add 中函数调用的顺序,看看它会抛出 AssertionError
。
查看更多示例跟踪调用顺序和不太详细的调用断言(链接已失效;替代:https://docs.python.org/3/library/unittest.mock.html#attaching-mocks-as-属性)
希望有帮助。
我今天需要这个答案,但是问题中的示例代码确实很难阅读,因为调用参数与测试范围内管理器和上的模拟名称相同。 这是有关此概念的官方文档,下面是针对非机器人的更清晰的示例。我正在修补的所有模块都是为了示例而编造的:
@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
如果您希望在订单或预期调用丢失时得到明确的断言失败消息,请改用以下断言行。
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)
])
看来不需要
attach_mock
。我从某处得到了以下技术,但现在找不到它:
order_of_calls_manager = mock.Mock()
with mock.patch.object(task, 'check_es_server'):
order_of_calls_manager.m1 = task.check_es_server = mock.Mock(return_value=True)
with mock.patch.object(task, 'check_index_exists'):
order_of_calls_manager.m2 = task.check_index_exists = mock.Mock(return_value=True)
with mock.patch.object(task, 'get_and_check_index_status_ldoc'):
order_of_calls_manager.m3 = task.get_and_check_index_status_ldoc = mock.Mock(return_value=True)
order_of_calls_manager.m4 = task.start_build_task = mock.Mock(return_value=True)
task.do()
calls = [
mock.call.m1(),
mock.call.m2(),
mock.call.m3(),
mock.call.m4(),
]
order_of_calls_manager.assert_has_calls(calls)
...如果在
task.do()
过程中调用了这些方法,但是以错误的顺序调用,则会失败。这似乎符合要求。
PS 不需要修补所有这些方法,从
task.start_build_task
可以看出。
更简洁的解决方案是将函数包装到一个类中,然后在测试中模拟该类。这将消除进行任何修补的需要(总是一个优点)。
# 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)
好处:
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)