【问题标题】:Checking call order across multiple mocks检查多个模拟的调用顺序
【发布时间】:2019-12-06 11:22:15
【问题描述】:

我正在尝试测试三个函数的调用顺序。

假设在模块 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 中应该已经有一些东西可以做到这一点,而我却错过了。

【问题讨论】:

    标签: python function mocking python-mock


    【解决方案1】:

    定义一个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查看更多示例

    希望对您有所帮助。

    【讨论】:

    • 一个重要通知 - 不要在 patch(...) 中设置 autospec=True。如果你设置 autospec=True attach_mock 不正确。您的示例中没有自动规范,但它经常存在于现实生活中的测试用例中。
    • 此答案忠实于问题中的代码,但由于没有描述性名称,因此难以阅读。我在下面添加了一个答案,以明确 attach_mocks 的工作原理。
    • @AndreyBelyak 评论现已过时 - 这是一个错误,现已修复(请参阅 here
    【解决方案2】:

    我今天需要这个答案,但是问题中的示例代码真的很难阅读,因为调用 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 创建的。必须通过attach_mock 附加具有名称的模拟,包括由patch 创建的模拟,此代码才能工作。如果您自己制作没有名称的 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)
    ])
    

    【讨论】:

      【解决方案3】:

      更简洁的解决方案是将您的函数包装到一个类中,然后在测试中模拟该类。这将消除进行任何修补的需要(总是一个加号)。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-03-12
        • 1970-01-01
        相关资源
        最近更新 更多