我如何模拟django信号处理程序?

时间:2012-10-28 19:39:27

标签: python django mocking signals django-signals

我有一个通过装饰器连接的signal_handler,这个非常简单:

@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,一切都按预期工作。

7 个答案:

答案 0 :(得分:15)

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

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

请注意,autospec=True中需要mock.patch才能使post_save.connect正确地处理MagicMock,否则django会引发一些异常,连接将失败。

答案 1 :(得分:15)

可能更好的想法是模拟信号处理程序中内部的功能而不是处理程序本身。使用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 :(得分:2)

答案 3 :(得分:1)

有一种方法可以用小班来模拟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)

答案 4 :(得分:1)

您可以通过在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()

这是更多的线,但它的效果也相当不错:)

答案 5 :(得分:1)

在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 :(得分:0)

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

我认为原因是:

在测试时,一些文件会导入信号的实现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