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.
Read my book Boost Your Django DX, freshly updated in November 2024.
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