Skip to content

Instantly share code, notes, and snippets.

@JordanReiter
Last active March 4, 2019 20:21
Show Gist options
  • Save JordanReiter/14a800ae524ac8928b8d55b8b78a40e9 to your computer and use it in GitHub Desktop.
Save JordanReiter/14a800ae524ac8928b8d55b8b78a40e9 to your computer and use it in GitHub Desktop.
Simple TestCase Mixin for testing whether signals are fired, and testing for specific signal values
from contextlib import contextmanager
from django.dispatch.dispatcher import Signal
class SignalTestMixin(object):
'''
Add this as a mixin to any Django TestCase
that needs to add testing for signals.
To add signal testing, you must wrap the code
in a with statement, like so:
with self.handle_signal(signal_to_handle):
code_that_should_call_signal
You can test whether the signal is fired
inside or outside of the context:
self.assertSignalFired(signal)
or confirm that the signal was never fired:
self.assertSignalNotFired(signal)
You can also test that the signal was sent
the expected Sender:
self.assertSignalFiredWithSender(signal, sender)
or arguments:
self.assertSignalFiredWithArgs(signal, args)
You can also check whether one or more of
the arguments matched with
self.assertSignalFiredArgsEqual(signal, args)
You can also check how many times a signal was
fired with
self.assertSignalFiredCount(signal, count)
If a signal is fired multiple times, this test
only captures the first set of arguments
sent in. Keeping track of all of the different
arguments and checking them is left as an
exercise for the reader.
'''
def setUp(self):
super(SignalTestMixin, self).setUp()
self.signals_fired = {}
self.signal_handlers = {}
def tearDown(self):
super(SignalTestMixin, self).tearDown()
self.signals_fired.clear()
@contextmanager
def handle_signal(self, signal, sender=None):
def create_handler():
def handler(sender=None, *args, **kwargs):
handled = kwargs.pop('signal')
self.signals_fired.setdefault(
handled,
{
'count': 0,
'sender': sender,
'kwargs': kwargs
}
)
self.signals_fired[handled]['count'] += 1
return handler
if not isinstance(signal, Signal):
raise RuntimeError(
"handle_signal can only accept Django signals, "
"this is a {}".format(signal.__class__.__name__)
)
try:
self.signal_handlers[signal] = create_handler()
signal.connect(self.signal_handlers[signal], sender=sender)
yield None
finally:
signal_handler = self.signal_handlers.pop(signal, None)
if signal_handler:
signal.disconnect(signal_handler)
def assertSignalFired(self, signal, msg=''):
self.assertIn(signal, self.signals_fired, 'Signal was not fired' + ': {}'.format(msg) if msg else '')
def assertSignalFiredCount(self, signal, firedCount, msg=''):
self.assertIn(signal, self.signals_fired, 'Signal was not fired' + ': {}'.format(msg) if msg else '')
fired = self.signals_fired[signal]
self.assertEqual(
fired['count'],
firedCount,
'The signal was supposed to be fired {} times but it was fired {} times.{}'.format(
firedCount,
fired['count'],
' : {}'.format(msg) if msg else {}
)
)
def assertSignalFiredWithSender(self, signal, sender, msg=''):
self.assertIn(signal, self.signals_fired, 'Signal was not fired' + ': {}'.format(msg) if msg else '')
fired = self.signals_fired[signal]
self.assertEqual(
fired['sender'],
sender,
'Signal fired, but with {} instead of {}{}'.format(
fired['sender'],
sender,
': {}'.format(msg) if msg else ''
)
)
def assertSignalFiredWithArgs(self, signal, expected_args, msg=''):
'''
For this test, the arguments sent to the receiver must match
the given arguments exactly.
'''
self.assertIn(signal, self.signals_fired, 'Signal was not fired' + ': {}'.format(msg) if msg else '')
fired = self.signals_fired[signal]
self.assertEqual(
fired['kwargs'],
expected_args,
'Signal fired, but with {} instead of {}{}'.format(
fired['kwargs'],
expected_args,
': {}'.format(msg) if msg else ''
)
)
def assertSignalFiredArgsEqual(self, signal, expected_args, msg=''):
'''
For this test, we only loop over the values provided in
expected_args and see if they match the corresponding arguments sent
to the receiver. This allows you to check if the receiver
was sent one or more expected values.
'''
self.assertIn(signal, self.signals_fired, 'Signal was not fired' + ' : {}'.format(msg) if msg else '')
fired = self.signals_fired[signal]
for exp_arg_key, exp_arg_value in expected_args.items():
self.assertIn(
exp_arg_key,
fired['kwargs'],
'Signal fired, but the value for {} was not sent by the signal.{}'.format(
exp_arg_key,
' : ' + msg if msg else ''
)
)
self.assertEqual(
exp_arg_value,
fired['kwargs'][exp_arg_key],
'Signal fired, but the value for {} was {} instead of the expected {}{}'.format(
exp_arg_key,
fired['kwargs'][exp_arg_key],
exp_arg_value,
' : ' + msg if msg else ''
)
)
def assertSignalNotFired(self, signal, msg=''):
self.assertNotIn(signal, self.signals_fired, 'Signal was not fired' + ': {}'.format(msg) if msg else '')
from django.dispatch.dispatcher import Signal
from django.test import TestCase
class TestSignalTestMixin(SignalTestMixin, TestCase):
'''
Tests for the test case itself, for completeness.
In a perfect world I would write real tests that
actually test that the assertions fail under incorrect
conditions but for some reason
with self.assertRaises(AssertionError)
does not appear to work.
'''
def setUp(self):
super(TestSignalTestMixin, self).setUp()
self.first_signal = Signal(providing_args=['name', 'color'])
self.second_signal = Signal()
def test_no_signals_nothing(self):
self.assertSignalNotFired(self.first_signal)
def test_handle_works(self):
self.assertNotIn(self.first_signal, self.signal_handlers)
with self.handle_signal(self.first_signal):
self.assertIn(self.first_signal, self.signal_handlers)
self.assertNotIn(self.first_signal, self.signal_handlers)
def test_signal_fired(self):
with self.handle_signal(self.first_signal):
self.first_signal.send(None, name="Signal Fired", color="blue")
self.assertIn(self.first_signal, self.signals_fired)
self.assertSignalFired(self.first_signal)
self.assertNotIn(self.first_signal, self.signal_handlers)
def test_signal_fired_count_1(self):
with self.handle_signal(self.first_signal):
self.first_signal.send(None, name="Signal Fired Count 1", color="blue")
self.assertSignalFiredCount(self.first_signal, 1)
def test_signal_fired_count_3(self):
with self.handle_signal(self.first_signal):
self.first_signal.send(None, name="Signal Fired Count 3, First Time", color="red")
self.first_signal.send(None, name="Signal Fired Count 3, Second Time", color="yellow")
self.first_signal.send(None, name="Signal Fired Count 3, Third Time", color="blue")
self.assertSignalFiredCount(self.first_signal, 3)
def test_signal_multple_fires_args(self):
with self.handle_signal(self.first_signal):
self.first_signal.send(None, name="Signal Fired Count 3, First Time", color="red")
self.first_signal.send(None, name="Signal Fired Count 3, Second Time", color="yellow")
self.first_signal.send(None, name="Signal Fired Count 3, Third Time", color="blue")
# only the first sent values should be recorded
self.assertSignalFiredArgsEqual(self.first_signal, {'color': "red"})
def test_signal_fired_before_context(self):
self.first_signal.send(None, name="Signal Fired", color="blue")
with self.handle_signal(self.first_signal):
self.assertNotIn(self.first_signal, self.signals_fired)
self.assertSignalNotFired(self.first_signal)
self.assertSignalNotFired(self.first_signal)
self.assertNotIn(self.first_signal, self.signal_handlers)
def test_signal_fired_after_context(self):
with self.handle_signal(self.first_signal):
self.assertNotIn(self.first_signal, self.signals_fired)
self.assertSignalNotFired(self.first_signal)
self.first_signal.send(None, name="Signal Fired", color="blue")
self.assertSignalNotFired(self.first_signal)
self.assertNotIn(self.first_signal, self.signal_handlers)
def test_signal_fired_check_after_context(self):
with self.handle_signal(self.first_signal):
self.first_signal.send(None, name="Signal Fired", color="blue")
self.assertIn(self.first_signal, self.signals_fired)
self.assertNotIn(self.first_signal, self.signal_handlers)
self.assertSignalFired(self.first_signal)
self.assertSignalNotFired(self.second_signal)
def test_signal_not_fired(self):
with self.handle_signal(self.first_signal):
self.assertNotIn(self.first_signal, self.signals_fired)
self.assertSignalNotFired(self.first_signal)
def test_signal_not_fired_check_after_context(self):
with self.handle_signal(self.first_signal):
self.assertNotIn(self.first_signal, self.signals_fired)
self.assertSignalNotFired(self.first_signal)
def test_signal_both_fired(self):
with self.handle_signal(self.first_signal):
self.first_signal.send(None, name="Signal Fired", color="blue")
self.second_signal.send(None)
self.assertIn(self.first_signal, self.signals_fired)
self.assertSignalFired(self.first_signal)
self.assertSignalNotFired(self.second_signal)
self.assertNotIn(self.second_signal, self.signal_handlers)
self.assertNotIn(self.first_signal, self.signal_handlers)
def test_not_signal_raises_runtime(self):
not_signal = object()
with self.assertRaises(RuntimeError):
with self.handle_signal(not_signal):
pass
def test_signal_both_fired_after_context(self):
with self.handle_signal(self.first_signal):
self.first_signal.send(None, name="Signal Fired", color="blue")
self.second_signal.send(None)
self.assertIn(self.first_signal, self.signals_fired)
self.assertSignalFired(self.first_signal)
self.assertSignalNotFired(self.second_signal)
def test_signal_fired_check_sender(self):
test_sender = object()
with self.handle_signal(self.first_signal, sender=test_sender):
self.first_signal.send(test_sender, name="Signal Fired", color="blue")
self.assertIn(self.first_signal, self.signals_fired)
self.assertSignalFiredWithSender(self.first_signal, test_sender)
def test_signal_fired_check_with_kwargs(self):
signal_kwargs = {
'name': 'Signal Fired Check Kwargs',
'color': 'orange',
}
with self.handle_signal(self.first_signal):
self.first_signal.send(None, **signal_kwargs)
self.assertIn(self.first_signal, self.signals_fired)
self.assertSignalFiredWithArgs(self.first_signal, signal_kwargs)
self.assertNotIn(self.first_signal, self.signal_handlers)
def test_signal_fired_check_one_kwarg(self):
signal_kwargs = {
'name': 'Signal Fired Check Kwargs',
'color': 'orange',
}
with self.handle_signal(self.first_signal):
self.first_signal.send(None, **signal_kwargs)
self.assertIn(self.first_signal, self.signals_fired)
self.assertSignalFiredArgsEqual(self.first_signal, {'name': signal_kwargs['name']})
self.assertSignalFiredArgsEqual(self.first_signal, {'color': signal_kwargs['color']})
self.assertNotIn(self.first_signal, self.signal_handlers)
def test_existing_signal(self):
from django.db.models.signals import pre_save
from django.contrib.auth import get_user_model
User = get_user_model()
with self.handle_signal(pre_save, User):
user = User.objects.create(username='testsignaluser')
self.assertSignalFired(pre_save)
self.assertSignalFiredWithSender(pre_save, User)
self.assertSignalFiredArgsEqual(pre_save, {'instance': user})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment