How do I mock a django signal handler?
Asked Answered
S

7

55

I have a signal_handler connected through a decorator, something like this very simple one:

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

What I want to do is to mock it with the mock library http://www.voidspace.org.uk/python/mock/ in a test, to check how many times django calls it. My code at the moment is something like:

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)

The problem here is that the original signal handler is called even if mocked, most likely because the @receiver decorator is storing a copy of the signal handler somewhere, so I'm mocking the wrong code.

So the question: how do I mock my signal handler to make my test work?

Note that if I change my signal handler to:

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)

and I mock _support_function instead, everything works as expected.

Shantel answered 28/10, 2012 at 19:39 Comment(0)
S
22

So, I ended up with a kind-of solution: mocking a signal handler simply means to connect the mock itself to the signal, so this exactly is what I did:

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

Notice that autospec=True in mock.patch is required in order to make post_save.connect to correctly work on a MagicMock, otherwise django will raise some exceptions and the connection will fail.

Shantel answered 29/10, 2012 at 9:47 Comment(6)
shouldn't assert_equal be assertEquals(...) ?Hypanthium
It depends on the testing suite you're using; django by default uses unittest, which has assertEquals as you say; I always use nose which is, in my opinion, superior in many aspects, and nose comes with a assert_equal. When writing my answer I copy/pasted from my production code, that's why you see assert_equal there. I edited the answer to respect the django default, thank you for pointing this outShantel
thanks! I also use nose but never realized that assert_equal existedHypanthium
This does not work for me either. The 'original' handler is still called.Bailes
You are not actually testing the signal, instead you ending up up testing the signal functionality of core django, but not your custom code. If you replace signal_handler_post_save_user with any other function the test case will pass, that's because you explicitly have made that handler function subscribe for the post_save event, then again checking back if that handler function is triggered. This way you aren't really making sure, that your handler function that you had written and was supposed to run on post_save event is getting triggered or not.Caffeine
what exactly is your "mock" object? unittest.mock?Barrage
A
28

Possibly a better idea is to mock out the functionality inside the signal handler rather than the handler itself. Using the OP's code:

@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

Then mock do_stuff:

with mock.patch('myapp.myfile.do_stuff') as mocked_handler:
    self.assert_equal(mocked_handler.call_count, 1)
Alanalana answered 2/10, 2015 at 18:7 Comment(2)
Why is this a better idea?Bailes
I figured it out this as well as I was digging out what the hell reason was. I found the function has been called, so It was fine for me to add a new method just for test, and to check if that method was called.Tsarism
S
22

So, I ended up with a kind-of solution: mocking a signal handler simply means to connect the mock itself to the signal, so this exactly is what I did:

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

Notice that autospec=True in mock.patch is required in order to make post_save.connect to correctly work on a MagicMock, otherwise django will raise some exceptions and the connection will fail.

Shantel answered 29/10, 2012 at 9:47 Comment(6)
shouldn't assert_equal be assertEquals(...) ?Hypanthium
It depends on the testing suite you're using; django by default uses unittest, which has assertEquals as you say; I always use nose which is, in my opinion, superior in many aspects, and nose comes with a assert_equal. When writing my answer I copy/pasted from my production code, that's why you see assert_equal there. I edited the answer to respect the django default, thank you for pointing this outShantel
thanks! I also use nose but never realized that assert_equal existedHypanthium
This does not work for me either. The 'original' handler is still called.Bailes
You are not actually testing the signal, instead you ending up up testing the signal functionality of core django, but not your custom code. If you replace signal_handler_post_save_user with any other function the test case will pass, that's because you explicitly have made that handler function subscribe for the post_save event, then again checking back if that handler function is triggered. This way you aren't really making sure, that your handler function that you had written and was supposed to run on post_save event is getting triggered or not.Caffeine
what exactly is your "mock" object? unittest.mock?Barrage
P
4

You can mock a django signal by mocking the ModelSignal class at django.db.models.signals.py like this:

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

That should do the trick. Note that this will mock ALL signals no matter which object you are using.

If by any chance you use the mocker library instead, it can be done like this:

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

It's more lines but it works pretty well too :)

Photostat answered 28/9, 2015 at 20:38 Comment(0)
J
3

take a look at mock_django . It has support for signals

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

Jeaz answered 29/10, 2012 at 5:41 Comment(1)
thank you @mossplix, I knew about django_mock, but I can't/don't want to add new requirements to the project, that's why I asked specifically a solution using python mockShantel
I
3

In django 1.9 you can mock all receivers with something like this

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

This replaces all receivers with mocks, eg ones you've registered, ones pluggable apps have registered and ones that django itself has registered. Don't be suprised if you use this on post_save and things start breaking.

You may want to inspect the receiver to determine if you actually want to mock it.

Impede answered 26/5, 2016 at 6:39 Comment(0)
S
2

There is a way to mock django signals with a small class.

You should keep in mind that this would only mock the function as a django signal handler and not the original function; for example, if a m2mchange trigers a call to a function that calls your handler directly, mock.call_count would not be incremented. You would need a separate mock to keep track of those calls.

Here is the class in question:

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

Example usage

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)
Sovereign answered 16/12, 2013 at 9:22 Comment(0)
N
1

As you mentioned, mock.patch('myapp.myfile._support_function') is correct but mock.patch('myapp.myfile.signal_handler_post_save_user') is wrong.

I think the reason is:

When init you test, some file import the signal's realization python file, then @receive decorator create a new signal connection.

In the test, mock.patch('myapp.myfile._support_function') will create another signal connection, so the original signal handler is called even if mocked.

Try to disconnect the signal connection before mock.patch('myapp.myfile._support_function'), like

post_save.disconnect(signal_handler_post_save_user)
with mock.patch("review.signals. signal_handler_post_save_user", autospec=True) as handler:
    #do stuff
Nicolanicolai answered 6/5, 2019 at 10:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.