Search code examples
djangodjango-testing

Django cache isolation when running tests in parallel


When I run tests in parallel, I get random failures because one test interferes with the cache of another test.

I can work around the problem with

@override_settings(
    CACHES={
        "default": {
            "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
            "LOCATION": "[random_string]",
        }
    },
)

Actually to make that smaller I created a @isolate_cache decorator that is a wrapper around override_settings.

But still I need to go and decorate a large number of test cases. This is error prone, because, as I said, the failures are random. I might run the test suite 100 times without error and think that everything is OK, but I still might have forgotten to decorate a test case, and at some point it will fail randomly.

I've also thought about creating my own TestCase subclass and use only that for all my test cases. This presents a similar problem: at some point someone would inherit from django.test.TestCase out of habit, and it might not fail for a long time. Besides, some of my tests inherit from rest_framework.test.APITestCase (or other classes), so there isn't a single test case subclass.

Is there any way to tell Django to run each test case in an isolated section of the cache once and for all?


Solution

  • You don't need "an isolated section of the cache", just to clear cache between tests.

    Here are a few ways.

    1. Subclass TestCase

    The question mentions this is not desired, but I should still mention this proper way.

    from django.core.cache import cache
    from django.test import TestCase
    
    
    class CacheClearTestCase(TestCase):
    
        def tearDown(self):
            # super().tearDown()
            cache.clear()
    

    2. Patch TestCase.tearDown

    Assuming subclasses that override tearDown call super().tearDown(), you could do this.

    Add this in manage.py before execute_from_command_line(sys.argv):

    if sys.argv[1] == 'test':
        from django.test import TestCase
        from django.core.cache import cache
        TestCase.tearDown = cache.clear
    

    3. Subclass TestSuite

    You can clear the cache after each test by subclassing TestSuite to override _removeTestAtIndex and setting DiscoverRunner.test_suite to that subclass.

    Add this in manage.py before execute_from_command_line(sys.argv):

    if sys.argv[1] == 'test':
        from unittest import TestSuite
        from django.core.cache import cache
        from django.test.runner import DiscoverRunner
    
        class CacheClearTestSuite(TestSuite):
            def _removeTestAtIndex(self, index):
                super()._removeTestAtIndex(index)
                cache.clear()
    
        DiscoverRunner.test_suite = CacheClearTestSuite
    

    Why you don't need an isolated section of the cache

    To be clear, this is not a problem caused by running tests in parallel.

    From https://docs.djangoproject.com/en/4.0/ref/django-admin/#cmdoption-test-parallel:

    --parallel [N]

    Runs tests in separate parallel processes.

    From https://docs.djangoproject.com/en/4.0/topics/cache/#local-memory-caching-1:

    Note that each process will have its own private cache instance, which means no cross-process caching is possible.