50

So I have various signals and handlers which are sent across apps. However, when I perform tests / go into 'testing mode', I want these handlers to be disabled.

Is there a Django-specific way of disabling signals/handlers when in testing mode? I can think of a very simple way (of including the handlers within an if TESTING clause) but I was wondering if there was a better way built into Django?...

user2564502
  • 937
  • 2
  • 10
  • 16
  • Perhaps you can declare a flag variable in your settings_test.py and only bind signals when this flag is to false. Like 'DEBUG' flag. An easy way is to create your own signal functions. I will be posted for most elegant approach. – dani herrera Aug 30 '13 at 12:11

9 Answers9

127

I found this question when looking to disable a signal for a set of test cases and Germano's answer lead me to the solution but it takes the opposite approach so I thought I'd add it.

In your test class:

class MyTest(TestCase):
    def setUp(self):
        # do some setup
        signals.disconnect(listener, sender=FooModel)

Instead of adding decision code to adding the signal I instead disabled it at the point of testing which feels like a nicer solution to me (as the tests should be written around the code rather than the code around the tests). Hopefully is useful to someone in the same boat!

Edit: Since writing this I've been introduced to another way of disabling signals for testing. This requires the factory_boy package (v2.4.0+) which is very useful for simplifying tests in Django. You're spoilt for choice really:

import factory
from django.db.models import signals

class MyTest(TestCase):
    @factory.django.mute_signals(signals.pre_save, signals.post_save)
    def test_something(self):

Caveat thanks to ups: it mutes signals inside factory and when an object is created, but not further inside test when you want to make explicit save() - signal will be unmuted there. If this is an issue then using the simple disconnect in setUp is probably the way to go.

coredumperror
  • 8,471
  • 6
  • 33
  • 42
krischan
  • 1,386
  • 2
  • 8
  • 7
  • 1
    Great! This should be the accepted answer, as the other requires rewriting source code (potentially code that isn't your own). – DylanYoung Jul 13 '16 at 16:16
  • @krischan: comment to factory - it mutes signals inside `factory` and when an object is created, but not further inside test when you want to make explicit `save()` - signal will be unmuted there. – Sławomir Lenart Jun 15 '17 at 16:05
  • @ups ah interesting I didn't know that! I'll edit the answer to include that note – krischan Jun 30 '17 at 11:07
  • 4
    @krischan I caution against the edited solution and prefer the first. Especially for disabling `pre_save` and `post_save`. Who knows what other critical things are listening on those. I think its better to **mute/mock the receiver (explicit) than the sender (could be connected to many receivers implicitly being muted)** – Ken Colton Jul 29 '17 at 19:12
  • That `signal.disconnect` should be `signals.disconnect` – physicalattraction Aug 24 '17 at 14:14
  • @KenColton how do you go about mocking the receiver? I tried patching it where the receiver is defined (since I don't import it in the view) but it didn't seem to patch the receiver. – Chuck Jan 08 '18 at 04:21
31

Here's a full example with imports on how to disable a specific signal in a test, if you don't want to use FactoryBoy.

from django.db.models import signals
from myapp.models import MyModel

class MyTest(TestCase):

    def test_no_signal(self):
        signals.post_save.disconnect(sender=MyModel, dispatch_uid="my_id")

        ... after this point, the signal is disabled ...

This should be matched against your receiver, this example would match this receiver:

@receiver(post_save, sender=MyModel, dispatch_uid="my_id")

I tried to disable the signal without specifying the dispatch_uid and it didn't work.

mrmuggles
  • 2,081
  • 4
  • 25
  • 44
  • Great answer, as it includes the import statements and the right way to invoke disconnect on `ModelSignal` (e.g: `post_save`). Here: +1 – Ramon K. Feb 11 '23 at 15:10
15

No, there is not. You can easily make a conditional connection though:

import sys

if not 'test' in sys.argv:
    signal.connect(listener, sender=FooModel)
Germano
  • 2,452
  • 18
  • 25
  • I was going to :)....Tried to accept it initially but had to wait before I could accept....so went and did something else first – user2564502 Aug 30 '13 at 12:29
  • 5
    Notice that tests always run with `DEBUG = False` ([docs](https://docs.djangoproject.com/en/1.4/topics/testing/#other-test-conditions)), so in this case the signals will still be connected in testing. – julen Feb 17 '14 at 10:32
  • This should not be the accepted answer because of the comment from @julen – Emil Stenström Jul 01 '14 at 11:48
  • 1
    @EmilStenström You are right. I just updated the answer. – Germano Jul 01 '14 at 11:53
10

All the answers didn't work for me except when I used Factory Boy by @krischan.

In my case, I want to disable signals that are part of another package django_elasticsearch_dsl which I couldn't locate the reciever or the dispatch_uid.

I don't want to add Factory Boy package, and I managed to disable the signals by reading its code to know how the signals are muted and it turned out very simple:

from django.db.models import signals

class MyTest(TestCase):
    def test_no_signal(self):
        signals.post_save.receivers = []

We can replace post_save with appropriate signal we want to disable, also we can put this in a setUp method for all tests.

abdelhalimresu
  • 417
  • 6
  • 15
4

I had a similar issue and wasn't able to successfully disconnect my signal using signals.post_save.disconnect(). Found this alternative approach that creates a decorator to override the SUSPEND_SIGNALS setting on specified tests and signals. Might be useful for anyone in the same boat.

First, create the decorator:

import functools

from django.conf import settings
from django.dispatch import receiver

def suspendingreceiver(signal, **decorator_kwargs):
    def our_wrapper(func):
        @receiver(signal, **decorator_kwargs)
        @functools.wraps(func)
        def fake_receiver(sender, **kwargs):
            if settings.SUSPEND_SIGNALS:
                return
            return func(sender, **kwargs)
        return fake_receiver
    return our_wrapper

Replace the usual @receiver decorator on your signal with the new one:

@suspendingreceiver(post_save, sender=MyModel)
def mymodel_post_save(sender, **kwargs):
    work()

Use Django's override_settings() on your TestCase:

@override_settings(SUSPEND_SIGNALS=True)
class MyTestCase(TestCase):
    def test_method(self):
        Model.objects.create()  # post_save_receiver won't execute

Thanks to Josh Smeaton, who wrote the blog.

qwertysmack
  • 188
  • 1
  • 12
4

If you connect receivers to signals in AppConfig.ready, which is recommended by documentation, see https://docs.djangoproject.com/en/2.2/topics/signals/#connecting-receiver-functions, you can create an alternative AppConfig for your tests with other signal receivers.

ziima
  • 706
  • 4
  • 17
3

You can do the following

from factory.django import mute_signals
from django.db.models import signals
def test_your_code():
    with mute_signals(signals.post_save):
        # ... code that doesn't trigger signals
KhaledMohamedP
  • 5,000
  • 3
  • 28
  • 26
3

The easiest way is using a fixture. just place this function inside your test file outside any class this will run automatically.

from django.db.models.signals import pre_save, post_save

@pytest.fixture(autouse=True) # Automatically use in tests.
def mute_signals(request):
    post_save.receivers = []
    pre_save.receivers = []

for more details see this https://www.cameronmaske.com/muting-django-signals-with-a-pytest-fixture/

Aleem
  • 549
  • 6
  • 13
0

Factory library has a function that you can use as fixture on functions. Here is an example on how to implement it.

from django.db.models import signals
from factory.django import mute_signals

from app.models import Model

@mute_signals(signals.pre_save, signals.post_save)
def function():
    instance = Model.objects.get(id=1)
    instance.attribute = "modify"
    instance.save()