How to test signals when using factory_boy with muted signals
Asked Answered
F

2

10

I am using factory_boy package and DjangoModelFactory to generate a factory model with muted signals

@factory.django.mute_signals(signals.post_save)
class SomeModelTargetFactory(DjangoModelFactory):
    name = factory.Sequence(lambda x: "Name #{}".format(x))
    ...

I have a post_save signal connected to the model:

def send_notification(sender, instance, created, **kwargs):
    if created:
        send_email(...)
post_save.connect(send_notification, SomeModel)

How can I test the signals works when I create an instance of the model using the factory class?

Finality answered 26/7, 2017 at 8:6 Comment(0)
R
17

Some solutions for the direct question. Followed by a caution.

A) Instead of turning off the signals, mock the side effects

@mock.patch('send_email')
def test_mocking_signal_side_effects(self, mocked_send_email):
    my_obj = SomeModelTargetFactory()

    # mocked version of send_email was called
    self.assertEqual(mocked_send_email.call_count, 1)

    my_obj.foo = 'bar'
    my_obj.save()

    # didn't call send_email again
    self.assertEqual(mocked_send_email.call_count, 1)

Note: mock was separate package before joining standard lib in 3.3

B) Use as context manager so you can selectively disable in your tests

This would leave the signals on by default, but you can selectively disable:

def test_without_signals(self):
    with factory.django.mute_signals(signals.post_save):
        my_obj = SomeModelTargetFactory()

        # ... perform actions w/o signals and assert  ...

C) Mute signals and an extended version of the base factory

class SomeModelTargetFactory(DjangoModelFactory):
    name = factory.Sequence(lambda x: "Name #{}".format(x))
    # ...


@factory.django.mute_signals(signals.post_save)
class SomeModelTargetFactoryNoSignals(SomeModelTargetFactory):
    pass

I've never tried this, but it seems like it should work. Additionally, if you just need the objects for a quick unit test where persistence isn't required, maybe FactoryBoy's BUILD strategy is a viable option.

Caution: Muting signals, especially like post_save can hide nasty bugs

There are easily findable references about how using signals in your own code can create a false sense of decoupling (post_save for example, essentially is the same as overriding and extending the save method. I'll let you research that to see if it applies to your use case.

Would definitely think twice about making it the default.

A safer approach is to "mute"/mock the receiver/side effect, not the sender.

The default Django model signals are used frequently by third party packages. Muting those can hide hard to track down bugs due to intra-package interaction.

Defining and calling (and then muting if needed) your own signals is better, but often is just re-inventing a method call. Sentry is a good example of signals being used well in a large codebase.

Solution A is by far the most explicit and safe. Solution B and C, without the addition of your own signal requires care and attention.

I wont say there are no use cases for muting post_save entirely. It should be an exception and an alert to maybe double check the need in the first place.

Rogation answered 29/7, 2017 at 18:48 Comment(1)
Thanks a lot for detail description and use cases.Finality
W
0

What I usually do is mute the signals on the factory the way you did, and then for the tests use Django save methods like so:

@mock.patch('send_email')
def test_signals(self, mocked_send_email):
    my_obj = SomeModelTargetFactory.build()
    # Following line will dispatch the signal that sends an email
    my_obj.save()

    # mocked version of send_email was called
    mocked_send_email.assert_called_once()

This is helpful so you don't have to worry about side effects on your signals like dispatching Celery tasks when testing other parts of your model.

Wendish answered 23/4 at 14:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.