Blog

“That some of us should venture to embark on a synthesis of facts and theories, albeit with second-hand and incomplete knowledge of some of them – and at the risk of making fools of ourselves” (Erwin Schrödinger)

Django: Speed Up Tests Slight-lier with In-Memory Sessions

An article by Adam Johnson recommended a small test speedup for Django unit tests that use the request test client. By bypassing the authentication backend and logging in the user directly with client.force_login(), and also disabling the last login signal handler, a few database queries are saved per-test run.

As I explored implementing this at work, I asked myself - could we skip the database entirely somehow? Does Django require a database for sessions?

The answer is no, Django does not require a database for sessions. In fact, Django supports several different session engines, including the cache - and the cache can be configured to use local memory as the backend!

Caveats

The documentation for configuring cached based sessions states:

The local-memory cache backend doesn’t retain data long enough to be a good choice, and it’ll be faster to use file or database sessions directly instead of sending everything through the file or database cache backends. Additionally, the local-memory cache backend is NOT multi-process safe, therefore probably not a good choice for production environments.

This is usually good and fine for an isolated test environment, but not in all scenarios. Multi-process test environments, such as running e2e tests with the pytest live_server fixture, would not be supported by this approach, since the test process and the server process do not share memory. Code that expects and interacts with database backed sessions will also (obviously) not work.

Configuration

To configure in-memory sessions for Django unit tests, set the following in your settings:

SESSION_ENGINE = "django.contrib.sessions.backends.cache"

You may also want to configure the following, if you are adding a new cache entry for this purpose:

SESSION_CACHE_ALIAS = "test-session-storage"

CACHES = {
    ...,
    "test-session-storage": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "test-session-storage", 
  }
}

Performance

The following test was used for calculating the number of queries in each scenario:

@pytest.mark.django_db
def test_login_queries(django_assert_num_queries):
    user = UserFactory(email="[email protected]", password="test")

    client = Client()

    with django_assert_num_queries(0):

        client.login(username=user.username, password="test")

        assert True, "This is a dummy test to test the number of queries."

Without Adam's changes (i.e. no signal disconnection, and using client.login()), the result is 20 queries. The base is fairly high due to a custom signal handler for user login.

With Adam's changes, the result is 14 queries.

With the local memory session backend, the result is 0 queries. Amazing!

Conclusion

While stimulating as an exercise, in practice with my work codebase and test suite I found negiglible time savings. The test suite includes maybe a dozen tests using the test client, while the vast majority test at the service layer.