Last active
March 4, 2019 20:21
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 '') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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