Kurt Peek
Kurt Peek

Reputation: 57521

In Django Factory Boy, is it possible to mute a specific receiver (not all signals)?

In our Django project, we have a receiver function for post_save signals sent by the User model:

@receiver(post_save, sender=User)
def update_intercom_attributes(sender, instance, **kwargs):
    # if the user is not yet in the app, do not attempt to setup/update their Intercom profile.
    if instance.using_app:
        intercom.update_intercom_attributes(instance)

This receiver calls an external API, and we'd like to disable it when generating test fixtures with factory_boy. As far as I can tell from https://factoryboy.readthedocs.io/en/latest/orms.html#disabling-signals, however, all one can do is mute all post_save signals, not a specific receiver.

At the moment, the way we are going about this is by defining an IntercomMixin which every test case inherits from (in the first position in the inheritance chain):

from unittest.mock import patch

class IntercomMixin:
    @classmethod
    def setUpClass(cls):
        cls.patcher = patch('lucy_web.lib.intercom.update_intercom_attributes')
        cls.intercomMock = cls.patcher.start()
        super().setUpClass()

    @classmethod
    def tearDownClass(cls):
        super().tearDownClass()
        cls.patcher.stop()

However, it is cumbersome and repetitive to add this mixin to every test case, and ideally, we'd like to build this patching functionality into the test factories themselves.

Is there any way to do this in Factory Boy? (I had a look at the source code (https://github.com/FactoryBoy/factory_boy/blob/2d735767b7f3e1f9adfc3f14c28eeef7acbf6e5a/factory/django.py#L256) and it seems like the __enter__ method is setting signal.receivers = []; perhaps this could be modified so that it accepts a receiver function and pops it out of the the signal.receivers list)?

Upvotes: 3

Views: 1345

Answers (3)

Premkumar chalmeti
Premkumar chalmeti

Reputation: 1018

I had a similar use case and looks like there is no direct support to mute the specific receivers of a signal.

I'd to implement a context manager to disconnect a receiver and restore it after the block of code is executed.

from contextlib import contextmanager
from django.dispatch import Signal
from typing import Any, Type


@contextmanager
def muted_receiver(
    signal: Type[Signal],
    receiver: callable,
    sender: Any,
    dispatch_uid=None,
    weak: bool = True,
    failsafe=False,
):
    disconnected = signal.disconnect(
        receiver=receiver,
        sender=sender,
        dispatch_uid=dispatch_uid,
    )
    if not disconnected and not failsafe:
        raise ValueError(f"Couldn't disconnect {signal}.")

    yield

    # restore the signal
    signal.connect(
        receiver=receiver,
        sender=sender,
        weak=weak,
        dispatch_uid=dispatch_uid,
    )
    assert receiver in signal._live_receivers(sender=sender)

Upvotes: 0

Roman
Roman

Reputation: 9441

In case you want to mute all signals of a type, you can configure that on your factory directly. For example:

from django.db.models.signals import post_save

@factory.django.mute_signals(post_save)
class UserFactory(DjangoModelFactory):
    ...

Upvotes: 1

Bil1
Bil1

Reputation: 438

For anyone looking for just this thing and finding themselves on this question you can find the solution here: https://stackoverflow.com/a/26490827/1108593

Basically... call @factory.django.mute_signals(post_save) on the test method itself; or in my case the setUpTestData method.

Test:

# test_models.py
from django.test import TestCase
from django.db.models.signals import post_save
from .factories import ProfileFactory
import factory

class ProfileTest(TestCase):
    @classmethod
    @factory.django.mute_signals(post_save)
    def setUpTestData(cls):
        ProfileFactory(id=1) # This won't trigger user creation.
        ...

Profile Factory:

#factories.py
import factory
from factory.django import DjangoModelFactory
from profiles.models import Profile
from authentication.tests.factories import UserFactory


class ProfileFactory(DjangoModelFactory):
    class Meta:
        model = Profile

    user = factory.SubFactory(UserFactory)

This allows your factories to keep working as expected and the tests to manipulate them as needed to test what they need.

Upvotes: 1

Related Questions