Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Connecting django signal handlers in tests

Using django-cacheops, I want to test that my views are getting cached as I intend them to be. In my test case I'm connecting cacheops cache_read signal to a handler that should increment a value in the cache for hits or misses. However, the signal is never fired. Does anyone know the correct way to connect a django signal handler in a testcase, purely for use in that testcase?

here's what I have so far

from cacheops.signals import cache_read

cache.set('test_cache_hits', 0)
cache.set('test_cache_misses', 0)

def cache_log(sender, func, hit, **kwargs):
    # never called
    if hit:
        cache.incr('test_cache_hits')
    else:
        cache.incr('test_cache_misses')


class BootstrapTests(TestCase):

    @classmethod
    def setUpClass(cls):
        super(BootstrapTests, cls).setUpClass()
        cache_read.connect(cache_log)
        assert cache_read.has_listeners()

    def test_something_that_should_fill_and_retrieve_cache(self):
        ....
        hits = cache.get('test_cache_hits') # always 0

I've also tried connecting the signal handler at the module level, and in the regular testcase setUp method, all with the same result.

EDIT: Here's my actual test code, plus the object I'm testing. I'm using the cached_as decorator to cache a function. This test is currently failing.

boostrap.py

class BootstrapData(object):

    def __init__(self, app, person=None):
        self.app = app

    def get_homepage_dict(self, context={}):

        url_name = self.app.url_name

        @cached_as(App.objects.filter(url_name=url_name), extra=context)
        def _get_homepage_dict():
            if self.app.homepage is None:
                return None

            concrete_module_class = MODULE_MAPPING[self.app.homepage.type]
            serializer_class_name = f'{concrete_module_class.__name__}Serializer'
            serializer_class = getattr(api.serializers, serializer_class_name)
            concrete_module = concrete_module_class.objects.get(module=self.app.homepage)
            serializer = serializer_class(context=context)
            key = concrete_module_class.__name__
            return {
                key: serializer.to_representation(instance=concrete_module)
            }
        return _get_homepage_dict()

test_bootstrap.py

class BootstrapDataTest(TestCase):

    def setUp(self):
        super(BootstrapDataTest, self).setUp()

        def set_signal(signal=None, **kwargs):
            self.signal_calls.append(kwargs)
        self.signal_calls = []
        cache_read.connect(set_signal, dispatch_uid=1, weak=False)
        self.app = self.setup_basic_app() # creates an 'App' model and saves it

    def tearDown(self):
        cache_read.disconnect(dispatch_uid=1)

    def test_boostrap_data_is_cached(self):

        obj = BootstrapData(self.app)
        obj.get_homepage_dict()

        # fails, self.signal_calls == []
        self.assertEqual(self.signal_calls, [{'sender': App, 'func': None, 'hit': False }])

        self.signal_calls = []

        obj.get_homepage_dict()
        self.assertEqual(self.signal_calls, [{'sender': App, 'func': None, 'hit': True}])
like image 597
bharling Avatar asked Jun 29 '17 09:06

bharling


People also ask

Are Django signals synchronous?

First, to dispel a misconception about signals, they are not executed asynchronously. There is no background thread or worker to execute them. Like most of Django, they are fully "synchronous".

What is Post_save in Django?

Post-save SignalThe post_save logic is just a normal function, the receiver function, but it's connected to a sender, which is the Order model. The code block below demonstrates the sample receiver function as a post-save. 1from django. db. models.

What is the use of the Post_delete signal in Django?

To notify another part of the application after the delete event of an object happens, you can use the post_delete signal.


2 Answers

I can't see why this is happening but I will try to make a useful answer anyway.

First, if you want to test whether cache works you shouldn't rely on its own side effects to check that, and signals are side effects of its primary function - preventing db calls. Try testing that:

def test_it_works(self):
    with self.assertNumQueries(1):
        obj.get_homepage_dict()

    with self.assertNumQueries(0):
        obj.get_homepage_dict()

Second, if you want to know what's going on you may dig in adding prints everywhere including cacheops code and see where it stops. Alternatively, you can make a test for me to see, the instruction is here https://github.com/Suor/django-cacheops#writing-a-test.

Last, your test is a bit wrong. For @cached_as() sender would be None and func would be decorated function.

like image 95
Suor Avatar answered Oct 12 '22 13:10

Suor


In this specific case, it turned out to be that my test cases subclassed django rest framework's APITestCase, which in turn subclasses django's SimpleTestCase.

looking in the cacheops sources, I found that those tests subclass TransactionTestCase, and switching out the test case fixed this issue.

Would be interested to know why this is the case but the issue is solved for now.

like image 1
bharling Avatar answered Oct 12 '22 13:10

bharling