Django: speed up tests slightly by disabling update_last_login

Django’s test client provides two methods to log in a user: login() and force_login(). The latter one is faster because it bypasses the authentication backend, including password hashing, and just sets a session to have the user logged in. Typically, you’d want to use it in setUp() like this:
from http import HTTPStatus
from django.test import TestCase
class CornCobListViewTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username="testuser",
password="12345",
)
def setUp(self):
self.client.force_login(self.user)
def test_success(self):
response = self.client.get("/cobs/")
assert response.status_code == HTTPStatus.OK
...
When profiling some client tests last week, I spotted that force_login() still took several milliseconds. Drilling down revealed that there was one query to create the session and another to update the user’s last_login field.
This update comes from the update_last_login() function (source):
def update_last_login(sender, user, **kwargs):
"""
A signal receiver which updates the last_login date for
the user logging in.
"""
user.last_login = timezone.now()
user.save(update_fields=["last_login"])
This receiver is conditionally registered in the django.contrib.auth app config (source):
class AuthConfig(AppConfig):
...
def ready(self):
...
# Register the handler only if UserModel.last_login is a field.
if isinstance(last_login_field, DeferredAttribute):
from .models import update_last_login
user_logged_in.connect(update_last_login, dispatch_uid="update_last_login")
...
Well, an extra query per force_login() is not a big deal, but with several thousand tests, it adds up to several seconds of the test suite’s ~8 minute runtime. Since the project in question does not use last_login, I decided to disconnect the signal receiver during tests with a session-scoped pytest fixture:
import pytest
@pytest.fixture(scope="session", autouse=True)
def disable_update_last_login():
"""
Disable the update_last_login signal receiver to reduce login overhead.
See: https://adamj.eu/tech/2024/09/18/django-test-speed-last-login/
"""
user_logged_in.disconnect(dispatch_uid="update_last_login")
yield
In projects using Django’s test framework, you can use the same approach inside a custom test runner.
So yeah, this change provides a small constant saving, but it is easy to achieve. A better solution would be to drop last_login from the custom user model, which would stop the receiver being registered, but that’s certainly more involved.
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts:
- Django: hoist repeated decorator definitions
- Django: a pattern for settings-configured API clients
- Django: avoid “useless use of
.all()”
Tags: django