Django cache isolation when running tests in parallel

Viewed 764

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?

2 Answers

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.

The easiest solution would be to have a separate settings file for tests that you can load in manage.py. This can also import all of your default settings.

manage.py

    settings = 'my_project.test_settings' if 'test' in sys.argv else 'my_project.settings'
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings)

test_settings.py

from .settings import *  # import default settings

# setting overrides here

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

If you need to do more settings overrides, especially for multiple environments, I would recommend using something like django-configurations.

Related