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:
33
api/tests.py
33
api/tests.py
@@ -18,11 +18,16 @@ from teams.models import Position, StaffAssignment, Team, TeamMembership, TeamPh
|
||||
ALLOWED_HOSTS=["rosterchief.app", "ajax-united.rosterchief.app", "rival-fc.rosterchief.app", "testserver"],
|
||||
)
|
||||
class ApiTestBase(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
# The tenant, its current season and its one team back every API test and are read
|
||||
# only. The handful of tests that do change them (deleting the season, giving the
|
||||
# club a logo) get their own copy from setUpTestData and are rolled back with the
|
||||
# per-test transaction.
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
today = timezone.localdate()
|
||||
self.season = Season.objects.create(club=self.club, start_date=today - datetime.timedelta(days=30), end_date=today + datetime.timedelta(days=300))
|
||||
self.team = Team.objects.create(club=self.club, name="First Team", short_name="1st")
|
||||
cls.season = Season.objects.create(club=cls.club, start_date=today - datetime.timedelta(days=30), end_date=today + datetime.timedelta(days=300))
|
||||
cls.team = Team.objects.create(club=cls.club, name="First Team", short_name="1st")
|
||||
|
||||
def api_get(self, path, **params):
|
||||
return self.client.get(f"/api/v1{path}", params, HTTP_HOST="ajax-united.rosterchief.app")
|
||||
@@ -211,11 +216,12 @@ class NewsApiTests(ApiTestBase):
|
||||
|
||||
|
||||
class TeamsApiTests(ApiTestBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.forward = Position.objects.create(club=self.club, name="Forward", short_name="FW", ordering=1)
|
||||
self.defense = Position.objects.create(club=self.club, name="Defense", short_name="DF", ordering=2)
|
||||
self.coach_position = Position.objects.create(club=self.club, name="Head Coach", short_name="HC", staff_position=True, management_position=True)
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
cls.forward = Position.objects.create(club=cls.club, name="Forward", short_name="FW", ordering=1)
|
||||
cls.defense = Position.objects.create(club=cls.club, name="Defense", short_name="DF", ordering=2)
|
||||
cls.coach_position = Position.objects.create(club=cls.club, name="Head Coach", short_name="HC", staff_position=True, management_position=True)
|
||||
|
||||
def test_list_teams(self):
|
||||
data = self.api_get("/teams/").json()
|
||||
@@ -323,10 +329,11 @@ class TeamsApiTests(ApiTestBase):
|
||||
|
||||
|
||||
class GamesApiTests(ApiTestBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.home_location = Location.objects.create(club=self.club, name="Home Arena", address="1 St", city="Town", zip_code="1000", country="BE", is_home=True)
|
||||
self.opponent = Opponent.objects.create(club=self.club, name="Rivals FC")
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
cls.home_location = Location.objects.create(club=cls.club, name="Home Arena", address="1 St", city="Town", zip_code="1000", country="BE", is_home=True)
|
||||
cls.opponent = Opponent.objects.create(club=cls.club, name="Rivals FC")
|
||||
|
||||
def make_game(self, **overrides):
|
||||
defaults = {"club": self.club, "title": "Game", "kind": Event.EventKind.GAME, "start": timezone.now() + datetime.timedelta(days=1), "opponent": self.opponent}
|
||||
|
||||
Reference in New Issue
Block a user