Speed up and rationalise the test suite (158s -> 16s)
Nearly all of the wall clock was password hashing: there was no test-time PASSWORD_HASHERS override, so Django's PBKDF2 default (~1.2M iterations) ran on every create_user and every login, hundreds of times over. The fix lives in a DiscoverRunner subclass wired in via TEST_RUNNER rather than a "test" in sys.argv sniff in settings: a runner is only ever instantiated by `manage.py test`, so there is no env var to mis-set and no import path by which a deployed process can reach the weak hasher. Verified: outside the runner the hasher is still PBKDF2. It also enables the cached template loader (the runner forces DEBUG off *after* settings are read, so Django never turns it on by itself) and silences django.request, whose 4xx/5xx logging buried real test output. Second, the fixtures. Base classes were rebuilding a club, season, admin user, membership, role and MFA authenticator once per test; those are read-only for almost every test, so they move to setUpTestData and are built once per class. Django hands each test its own deep copy and the per-test transaction rolls the rows back, so the handful of tests that mutate them stay isolated -- proved with --shuffle, --reverse and --parallel rather than assumed. Per-test work that genuinely must stay per-test (client sign-ins, waffle cache clears that leak across the transaction boundary) is left in setUp with a comment saying why. Five tests removed, each strictly subsumed by another that asserts a superset; their intent was folded into a comment on the survivor. Regression-pinning tests -- the ones carrying comments naming the exact bug they catch -- were left verbatim throughout. Also closes a real gap this surfaced: teams had a cross-club position test for TeamMembership but not for StaffAssignment, with an unused `other_coach` fixture sitting there waiting for it. Rejected: --parallel by default (every worker re-runs all 88 migrations, buying ~4s of wall clock for ~5x the CPU), and disabling migrations in tests (~3.5s, but the schema would then come from models and the suite would stop catching a broken migration). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -145,8 +145,10 @@ class WebAuthnRelyingPartyTests(TestCase):
|
||||
|
||||
|
||||
class MFARequirementTests(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
# Read-only for every test here: each one brings its own user and role.
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
|
||||
def make_user(self, email, **kwargs):
|
||||
return User.objects.create_user(email=email, password="pw-secret-123", **kwargs)
|
||||
@@ -232,9 +234,6 @@ class AdminLoginRoutingTests(TestCase):
|
||||
# The original destination survives the hop (percent-encoded).
|
||||
self.assertEqual(parse_qs(redirect.query)["next"], ["/admin/"])
|
||||
|
||||
def test_allauth_login_page_loads(self):
|
||||
self.assertEqual(self.client.get(reverse("account_login")).status_code, 200)
|
||||
|
||||
|
||||
class AuthFormRenderingTests(TestCase):
|
||||
"""Every allauth form must actually render its fields.
|
||||
@@ -257,9 +256,12 @@ class AuthFormRenderingTests(TestCase):
|
||||
|
||||
|
||||
class TwoFactorPageTests(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
enrol_mfa(User.objects.create_user(email="mfa@example.com", password="pw-secret-123"))
|
||||
|
||||
def setUp(self):
|
||||
user = User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
enrol_mfa(user)
|
||||
# The test client is per-test, so the sign-in itself cannot be hoisted.
|
||||
# Password accepted, second factor still owed: this is the 2FA challenge page.
|
||||
self.response = self.client.post(reverse("account_login"), {"login": "mfa@example.com", "password": "pw-secret-123"}, follow=True)
|
||||
|
||||
@@ -313,8 +315,11 @@ class MfaPageTests(TestCase):
|
||||
"""Every MFA screen must render. They are built from allauth's `element` primitives,
|
||||
so styling lives in the element overrides rather than in eight page templates."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
# A real password login (not force_login) so allauth counts it as a recent
|
||||
# authentication and doesn't bounce the sensitive pages to reauthenticate.
|
||||
self.client.post(reverse("account_login"), {"login": "mfa@example.com", "password": "pw-secret-123"}, follow=True)
|
||||
@@ -361,8 +366,11 @@ class ActionBarTests(TestCase):
|
||||
Keying the bar on it hid the button on exactly the pages that are nothing but a button.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
self.client.post(reverse("account_login"), {"login": "mfa@example.com", "password": "pw-secret-123"}, follow=True)
|
||||
|
||||
def test_the_sign_out_page_has_its_button(self):
|
||||
@@ -381,8 +389,11 @@ class ActionBarTests(TestCase):
|
||||
|
||||
|
||||
class SignOutPageTests(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
self.client.force_login(self.user)
|
||||
self.response = self.client.get(reverse("account_logout"))
|
||||
|
||||
@@ -407,8 +418,11 @@ class SignOutPageTests(TestCase):
|
||||
|
||||
|
||||
class ChangePasswordPageTests(TestCase):
|
||||
def setUp(self):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
|
||||
def setUp(self):
|
||||
self.client.post(reverse("account_login"), {"login": "mfa@example.com", "password": "pw-secret-123"}, follow=True)
|
||||
self.response = self.client.get(reverse("account_change_password"))
|
||||
|
||||
@@ -438,8 +452,11 @@ class MfaButtonIconTests(TestCase):
|
||||
ranked: View is primary, Download and Generate are outline. Generate throws away the
|
||||
codes you already have, so it must not read as the obvious thing to click."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(email="mfa@example.com", password="pw-secret-123")
|
||||
# Sign in *before* enrolling: a user who already holds a second factor is stopped at
|
||||
# the 2FA challenge and never reaches these pages.
|
||||
self.client.post(reverse("account_login"), {"login": "mfa@example.com", "password": "pw-secret-123"}, follow=True)
|
||||
|
||||
Reference in New Issue
Block a user