Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test signals when using factory_boy with muted signals

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?

like image 625
Gabriel Muj Avatar asked Dec 24 '22 15:12

Gabriel Muj


1 Answers

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.

like image 162
Ken Colton Avatar answered Mar 07 '23 16:03

Ken Colton