【问题标题】:How do I mock a django signal handler?如何模拟 django 信号处理程序?
【发布时间】:2012-10-28 19:39:27
【问题描述】:

我有一个通过装饰器连接的信号处理程序,就像这样非常简单:

@receiver(post_save, sender=User, 
          dispatch_uid='myfile.signal_handler_post_save_user')
def signal_handler_post_save_user(sender, *args, **kwargs):
   # do stuff

我想做的是在测试中使用模拟库http://www.voidspace.org.uk/python/mock/ 模拟它,以检查 django 调用它的次数。我现在的代码是这样的:

def test_cache():
    with mock.patch('myapp.myfile.signal_handler_post_save_user') as mocked_handler:
        # do stuff that will call the post_save of User
    self.assert_equal(mocked_handler.call_count, 1)

这里的问题是即使模拟了原始信号处理程序也会被调用,很可能是因为@receiver 装饰器在某处存储了信号处理程序的副本,所以我模拟了错误的代码。

所以问题是:我如何模拟我的信号处理程序以使我的测试工作?

请注意,如果我将信号处理程序更改为:

def _support_function(*args, **kwargs):
    # do stuff

@receiver(post_save, sender=User, 
          dispatch_uid='myfile.signal_handler_post_save_user')
def signal_handler_post_save_user(sender, *args, **kwargs):
   _support_function(*args, **kwargs)

我改为模拟 _support_function,一切都按预期工作。

【问题讨论】:

    标签: python django mocking signals django-signals


    【解决方案1】:

    可能更好的主意是模拟信号处理程序内部的功能,而不是处理程序本身。使用 OP 的代码:

    @receiver(post_save, sender=User, dispatch_uid='myfile.signal_handler_post_save_user')
    def signal_handler_post_save_user(sender, *args, **kwargs):
      do_stuff()  # <-- mock this
    
    def do_stuff():
       ... do stuff in here
    

    然后模拟do_stuff:

    with mock.patch('myapp.myfile.do_stuff') as mocked_handler:
        self.assert_equal(mocked_handler.call_count, 1)
    

    【讨论】:

    • 为什么这是一个更好的主意?
    • 我想通了这一点,同时我也在挖掘到底是什么原因。我发现函数已经被调用了,所以我可以添加一个新方法来测试,并检查该方法是否被调用。
    【解决方案2】:

    所以,我最终得到了一种解决方案:模拟信号处理程序只是意味着将模拟本身连接到信号,所以这正是我所做的:

    def test_cache():
        with mock.patch('myapp.myfile.signal_handler_post_save_user', autospec=True) as mocked_handler:
            post_save.connect(mocked_handler, sender=User, dispatch_uid='test_cache_mocked_handler')
            # do stuff that will call the post_save of User
        self.assertEquals(mocked_handler.call_count, 1)  # standard django
        # self.assert_equal(mocked_handler.call_count, 1)  # when using django-nose
    

    请注意,mock.patch 中的autospec=True 是必需的,以使post_save.connect 能够在MagicMock 上正确工作,否则 django 会引发一些异常并且连接将失败。

    【讨论】:

    • 不应该 assert_equalassertEquals(...) 吗?
    • 这取决于您使用的测试套件; django 默认使用unittest,正如你所说的assertEquals;我总是使用nose,在我看来,它在许多方面都比较出色,而鼻子带有assert_equal。在写我的答案时,我从我的生产代码中复制/粘贴,这就是你在那里看到assert_equal 的原因。我编辑了答案以尊重 django 默认值,感谢您指出这一点
    • 谢谢!我也使用鼻子,但从未意识到 assert_equal 存在
    • 这对我也不起作用。 '原始' 处理程序仍然被调用。
    • 您实际上并没有测试信号,而是您最终测试了核心 django 的信号功能,而不是您的自定义代码。如果您将signal_handler_post_save_user 替换为测试用例将通过的任何其他函数,那是因为您明确地使该处理函数订阅post_save 事件,然后再次检查该处理函数是否被触发。这样您就不能真正确定您编写的并且应该在 post_save 事件上运行的处理程序函数是否被触发。
    【解决方案3】:

    您可以通过模拟 django.db.models.signals.py 的 ModelSignal 类来模拟 django 信号,如下所示:

    @patch("django.db.models.signals.ModelSignal.send")
    def test_overwhelming(self, mocker_signal):
        obj = Object()
    

    这应该可以解决问题。请注意,无论您使用哪个对象,这都会模拟所有信号。

    如果您有机会改用mocker 库,则可以这样做:

    from mocker import Mocker, ARGS, KWARGS
    
    def test_overwhelming(self):
        mocker = Mocker()
        # mock the post save signal
        msave = mocker.replace("django.db.models.signals")
        msave.post_save.send(KWARGS)
        mocker.count(0, None)
    
        with mocker:
            obj = Object()
    

    它的行数更多,但效果也很好:)

    【讨论】:

      【解决方案4】:

      【讨论】:

      • 谢谢@mossplix,我知道 django_mock,但我不能/不想向项目添加新要求,这就是为什么我专门询问使用 python mock 的解决方案
      【解决方案5】:

      在 django 1.9 中,您可以使用类似的方式模拟所有接收器

      # replace actual receivers with mocks
      mocked_receivers = []
      for i, receiver in enumerate(your_signal.receivers):
          mock_receiver = Mock()
          your_signal.receivers[i] = (receiver[0], mock_receiver)
          mocked_receivers.append(mock_receiver)
      
      ...  # whatever your test does
      
      # ensure that mocked receivers have been called as expected
      for mocked_receiver in mocked_receivers:
          assert mocked_receiver.call_count == 1
          mocked_receiver.assert_called_with(*your_args, sender="your_sender", signal=your_signal, **your_kwargs)
      

      这会将所有接收器替换为模拟,例如您已注册的接收器、可插入应用程序已注册的接收器以及 django 本身已注册的接收器。如果您在post_save 上使用它,请不要感到惊讶并且事情开始破裂。

      您可能需要检查接收器以确定您是否真的要模拟它。

      【讨论】:

        【解决方案6】:

        有一种方法可以用一个小类来模拟 django 信号。

        您应该记住,这只会将函数模拟为 django 信号处理程序,而不是原始函数;例如,如果 m2mchange 触发对直接调用您的处理程序的函数的调用,则 mock.call_count 不会增加。您需要一个单独的模拟来跟踪这些调用。

        这是有问题的课程:

        class LocalDjangoSignalsMock():
            def __init__(self, to_mock):
                """ 
                Replaces registered django signals with MagicMocks
        
                :param to_mock: list of signal handlers to mock
                """
                self.mocks = {handler:MagicMock() for handler in to_mock}
                self.reverse_mocks = {magicmock:mocked
                                      for mocked,magicmock in self.mocks.items()}
                django_signals = [signals.post_save, signals.m2m_changed]
                self.registered_receivers = [signal.receivers
                                             for signal in django_signals]
        
            def _apply_mocks(self):
                for receivers in self.registered_receivers:
                    for receiver_index in xrange(len(receivers)):
                        handler = receivers[receiver_index]
                        handler_function = handler[1]()
                        if handler_function in self.mocks:
                            receivers[receiver_index] = (
                                handler[0], self.mocks[handler_function])
        
            def _reverse_mocks(self):
                for receivers in self.registered_receivers:
                    for receiver_index in xrange(len(receivers)):
                        handler = receivers[receiver_index]
                        handler_function = handler[1]
                        if not isinstance(handler_function, MagicMock):
                            continue
                        receivers[receiver_index] = (
                            handler[0], weakref.ref(self.reverse_mocks[handler_function]))
        
            def __enter__(self):
                self._apply_mocks()
                return self.mocks
        
            def __exit__(self, *args):
                self._reverse_mocks()
        

        示例用法

        to_mock = [my_handler]
        with LocalDjangoSignalsMock(to_mock) as mocks:
            my_trigger()
            for mocked in to_mock:
                assert(mocks[mocked].call_count)
                # 'function {0} was called {1}'.format(
                #      mocked, mocked.call_count)
        

        【讨论】:

          【解决方案7】:

          正如你所说, mock.patch('myapp.myfile._support_function') 是正确的,但 mock.patch('myapp.myfile.signal_handler_post_save_user') 是错误的。

          我认为原因是:

          在init你测试的时候,一些文件导入信号的实现python文件,然后@receive装饰器创建一个新的信号连接。

          在测试中,mock.patch('myapp.myfile._support_function') 将创建另一个信号连接,因此即使被模拟,也会调用原始信号处理程序。

          尝试在mock.patch('myapp.myfile._support_function')之前断开信号连接,如

          post_save.disconnect(signal_handler_post_save_user)
          with mock.patch("review.signals. signal_handler_post_save_user", autospec=True) as handler:
              #do stuff
          

          【讨论】:

            猜你喜欢
            • 2017-03-20
            • 2013-03-15
            • 1970-01-01
            • 2013-01-07
            • 1970-01-01
            • 2012-03-07
            • 1970-01-01
            • 2018-02-04
            • 1970-01-01
            相关资源
            最近更新 更多