[Django]-Proper way to test Django signals

9👍

I’ve resolved the problem by myself. I think that the best solution is following:

    def test_form_should_post_proper_data_via_signal(self):
        # define the local listener
        def question_posted_listener(sender, form_data, **kwargs):
            self.name = form_data['name']

        # prepare fake data
        form_data = {'name': 'Jan Nowak'}

        # connect & send the signal
        signals.question_posted.connect(question_posted_listener, sender='test')
        signals.question_posted.send(sender='test', form_data=form_data)

        # check results
        eq_(self.name, 'Jan Nowak')
👤bx2

39👍

Simplest way to do what you asked in 2015:

from unittest.mock import patch

@patch('full.path.to.signals.question_posted.send')
def test_question_posted_signal_triggered(self, mock):
    form = YourForm()
    form.cleaned_data = {'name': 'Jan Nowak'}
    form.save()

    # Check that your signal was called.
    self.assertTrue(mock.called)

    # Check that your signal was called only once.
    self.assertEqual(mock.call_count, 1)

    # Do whatever else, like actually checking if your signal logic did well.

And with that, you just tested that your signal was properly triggered.

23👍

I have an alternative suggestion using the mock library, which is now part of the unittest.mock standard library in Python 3 (if you’re using Python 2, you’ll have to pip install mock).

try:
    from unittest.mock import MagicMock
except ImportError:
    from mock import MagicMock

def test_form_should_post_proper_data_via_signal(self):
    """
    Assert signal is sent with proper arguments
    """ 

    # Create handler
    handler = MagicMock()
    signals.question_posted.connect(handler, sender='test')

    # Post the form or do what it takes to send the signal
    signals.question_posted.send(sender='test', form_data=form_data)

    # Assert the signal was called only once with the args
    handler.assert_called_once_with(signal=signals.question_posted, form_data=form_data, sender="test")

The essential part of the suggestion is to mock a receiver, then test whether or not your signal is being sent to that receiver, and called only once. This is great, especially if you have custom signals, or you’ve written methods that send signals and you want to ensure in your unit tests that they are being sent.

8👍

You need to:

  • assert a signal was emited with proper arguments and,
  • a specific number of times and,
  • in appropriate order.

You can use mock_django app which provides a mock for signals.

Example:

from mock import call


def test_install_dependency(self):
    with mock_signal_receiver(post_app_install) as install_receiver:
        self.env.install(self.music_app)
        self.assertEqual(install_receiver.call_args_list, [
            call(signal=post_app_install, sender=self.env,
                app=self.ukulele_app),
            call(signal=post_app_install, sender=self.env,
                app=self.music_app),
        ])
👤jpic

7👍

The purpose of this isn’t to test the underlying signalling mechanism, but rather is an important unit test to ensure that whatever signal your method is supposed to emit is actually emitted with the proper arguments. In this case, it seems a little trivial since its an internal django signal, but imagine if you wrote the method that was emitting a custom signal.

👤dana

1👍

Why do you test your framework? Django already have unit tests for signal dispatcher. If you don’t believe that your framework is fine just attach it unit tests to yours test runner.

1👍

For my part, I wouldn’t test that the signal is sent. I would test the intended effect of the signals processing.

In my use case, the signals are used to update a Produit.qte attribute when, say, Order.qte_shipped is upated. (E.g. when we fill an order, I want the qte of the given product to be subtracted from the corresponding product for that order).

Thus I do something like this in signals.py:

@receiver(post_save, sender='orders.Order')
@disable_for_loaddata
def quantity_adjust_order(sender, **kwargs):
    # retrieve the corresponding product for that order
    # subtract Order.qte_shipped from Produit.qte
    # save the updated Produit

What I actually test is that Produit.qte is correctly updated when I ship an Order. I do not test that the signals works; that’s just one of the things that COULD explain why test_order_ship_updates_product() failed.

I somewhat agree with what @Piotr Czapla said; you’re kind of trying to test the framework. Test the effect on your code instead.

Leave a comment