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:
@@ -31,8 +31,9 @@ from .models import (
|
||||
|
||||
|
||||
class ProductSlugTests(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
|
||||
def test_slug_auto_populated_from_name(self):
|
||||
product = Product.objects.create(club=self.club, name="Home Jersey")
|
||||
@@ -70,9 +71,10 @@ class ProductSlugTests(TestCase):
|
||||
|
||||
|
||||
class OpenCartConstraintTests(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
self.user = User.objects.create_user(email="shopper@example.com", password="pw")
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
cls.user = User.objects.create_user(email="shopper@example.com", password="pw")
|
||||
|
||||
def test_only_one_open_cart_per_user_per_club(self):
|
||||
Cart.objects.create(club=self.club, user=self.user)
|
||||
@@ -113,10 +115,11 @@ class CartItemTests(TestCase):
|
||||
|
||||
|
||||
class OrderNumberTests(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
self.member = Member.objects.create(first_name="Jane", last_name="Doe")
|
||||
self.year = timezone.now().year
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
cls.member = Member.objects.create(first_name="Jane", last_name="Doe")
|
||||
cls.year = timezone.now().year
|
||||
|
||||
def make_order(self, **kwargs):
|
||||
kwargs.setdefault("club", self.club)
|
||||
@@ -195,12 +198,13 @@ class OrderNumberTests(TestCase):
|
||||
|
||||
|
||||
class ShopEntitiesTestBase(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
self.member = Member.objects.create(first_name="Jane", last_name="Doe")
|
||||
self.product = Product.objects.create(club=self.club, name="Home Jersey")
|
||||
self.order = Order.objects.create(club=self.club, purchaser=self.member, total=Decimal("50.00"))
|
||||
self.year = timezone.now().year
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
cls.member = Member.objects.create(first_name="Jane", last_name="Doe")
|
||||
cls.product = Product.objects.create(club=cls.club, name="Home Jersey")
|
||||
cls.order = Order.objects.create(club=cls.club, purchaser=cls.member, total=Decimal("50.00"))
|
||||
cls.year = timezone.now().year
|
||||
|
||||
|
||||
class OrderLineTests(ShopEntitiesTestBase):
|
||||
@@ -241,9 +245,10 @@ class DiscountTests(ShopEntitiesTestBase):
|
||||
|
||||
|
||||
class AppliedDiscountTests(ShopEntitiesTestBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.discount = Discount.objects.create(club=self.club, name="Sibling")
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
cls.discount = Discount.objects.create(club=cls.club, name="Sibling")
|
||||
|
||||
def apply(self, **kwargs):
|
||||
kwargs.setdefault("order", self.order)
|
||||
@@ -311,15 +316,16 @@ class InvoiceTests(ShopEntitiesTestBase):
|
||||
|
||||
|
||||
class ClubScopeValidationTests(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
self.other = Club.objects.create(name="Rival FC", slug="rival-fc")
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
cls.other = Club.objects.create(name="Rival FC", slug="rival-fc")
|
||||
today = timezone.localdate()
|
||||
self.season = Season.objects.create(club=self.club, start_date=today, end_date=today + timedelta(days=300))
|
||||
self.other_season = Season.objects.create(club=self.other, start_date=today, end_date=today + timedelta(days=300))
|
||||
self.member = Member.objects.create(first_name="Jane", last_name="Doe")
|
||||
ClubMembership.objects.create(club=self.club, member=self.member, season=self.season)
|
||||
self.stranger = Member.objects.create(first_name="Stray", last_name="Ger")
|
||||
cls.season = Season.objects.create(club=cls.club, start_date=today, end_date=today + timedelta(days=300))
|
||||
cls.other_season = Season.objects.create(club=cls.other, start_date=today, end_date=today + timedelta(days=300))
|
||||
cls.member = Member.objects.create(first_name="Jane", last_name="Doe")
|
||||
ClubMembership.objects.create(club=cls.club, member=cls.member, season=cls.season)
|
||||
cls.stranger = Member.objects.create(first_name="Stray", last_name="Ger")
|
||||
|
||||
def make_cart(self, club):
|
||||
user = User.objects.create_user(email=f"u-{club.slug}@example.com", password="pw")
|
||||
@@ -442,12 +448,13 @@ class ClubScopeValidationTests(TestCase):
|
||||
|
||||
|
||||
class AdminScopingTests(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
self.other = Club.objects.create(name="Rival FC", slug="rival-fc")
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
cls.other = Club.objects.create(name="Rival FC", slug="rival-fc")
|
||||
today = timezone.localdate()
|
||||
self.season = Season.objects.create(club=self.club, start_date=today, end_date=today + timedelta(days=300))
|
||||
self.other_season = Season.objects.create(club=self.other, start_date=today, end_date=today + timedelta(days=300))
|
||||
cls.season = Season.objects.create(club=cls.club, start_date=today, end_date=today + timedelta(days=300))
|
||||
cls.other_season = Season.objects.create(club=cls.other, start_date=today, end_date=today + timedelta(days=300))
|
||||
|
||||
def test_fk_dropdown_scoped_to_object_club(self):
|
||||
product = Product.objects.create(club=self.club, name="Jersey")
|
||||
|
||||
Reference in New Issue
Block a user