51

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

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

4

7 回答 7

25

可能更好的主意是模拟信号处理程序内部的功能,而不是处理程序本身。使用 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)
于 2015-10-02T18:07:43.247 回答
18

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

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

请注意,为了正确地在 a 上工作,autospec=True需要in ,否则 django 将引发一些异常并且连接将失败。mock.patchpost_save.connectMagicMock

于 2012-10-29T09:47:37.533 回答
4

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

@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()

它的线条更多,但效果也很好:)

于 2015-09-28T20:38:32.810 回答
3

看看 mock_django 。它支持信号

https://github.com/dcramer/mock-django/blob/master/tests/mock_django/signals/tests.py

于 2012-10-29T05:41:07.987 回答
3

在 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如果您使用它并且事情开始破裂,请不要感到惊讶。

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

于 2016-05-26T06:39:11.187 回答
2

有一种方法可以用小班模拟 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)
于 2013-12-16T09:22:12.320 回答
1

正如你所提到的, 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
于 2019-05-06T10:00:09.720 回答