From 6f68092d712c72986f5797fc794528cd5561adab Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 3 Feb 2026 14:38:58 +0800 Subject: [PATCH] test: implement per-test isolation for integration tests Adds ServiceManager.reset_test_data() method for per-test isolation, resolving the test_00_* naming hack required for tests that expect empty state. Changes: - Add ServiceManager.reset_test_data() to reset storage between tests - Move reset from tearDownClass() to tearDown() in test_assignments.py - Re-create test tokens in setUp() after storage reset - Remove test_00_* prefix from test_list_assignments_empty Performance impact: Negligible (~0.04s increase for 12-test suite) - SQLite in-memory reset: just closes/reopens connection - MemoryCollection reset: just clears dict - Flask apps preserved (avoids blueprint re-registration issues) Fixes #335 Co-Authored-By: Claude Opus 4.5 --- tests/fixtures/services.py | 23 +++++++++++ tests/integration/api/test_assignments.py | 47 ++++++++++++----------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/tests/fixtures/services.py b/tests/fixtures/services.py index 3aec5209..06dff986 100644 --- a/tests/fixtures/services.py +++ b/tests/fixtures/services.py @@ -157,6 +157,29 @@ def setup(self): return self + def reset_test_data(self): + """Reset test storage for per-test isolation. + + This method clears all test storage (SQLite in-memory DB, memory collections) + and re-initializes services. Use this in tearDown() for per-test isolation. + + WARNING: This clears auth credentials, so tests using bearer tokens will need + to re-create their tokens after calling this method. + + For tests that use authentication, consider using unique identifiers per test + (e.g., filtering by created_by) instead of full reset, or re-create the token + in setUp() after calling this in tearDown(). + """ + import campus.storage.testing + + # Reset storage (clears SQLite in-memory DB and memory collections) + campus.storage.testing.reset_test_storage() + + # Re-initialize auth and yapper services + # These are idempotent and will recreate necessary tables/collections + auth.init() + yapper.init() + def close(self): """Clean up service instances and resources. diff --git a/tests/integration/api/test_assignments.py b/tests/integration/api/test_assignments.py index f31e2c5d..e326cc92 100644 --- a/tests/integration/api/test_assignments.py +++ b/tests/integration/api/test_assignments.py @@ -27,17 +27,32 @@ def setUpClass(cls): raise RuntimeError("Expected Flask app from service manager") cls.app = api_app + cls.user_id = schema.UserID("test.user@campus.test") - # Initialize credentials storage (needed for token creation) - # This may have been cleared by a previous test class - from campus.auth import resources as auth_resources - auth_resources.credentials.init_storage() - auth_resources.user.init_storage() + def setUp(self): + """Set up test environment before each test.""" + self.client = self.app.test_client() + + # Set up test context + self.app_context = self.app.app_context() + self.app_context.push() # Create test user token for bearer auth - cls.user_id = schema.UserID("test.user@campus.test") - cls.token = create_test_token(cls.user_id) - cls.auth_headers = get_bearer_auth_headers(cls.token) + # Re-create before each test since tearDown() resets storage + self.token = create_test_token(self.user_id) + self.auth_headers = get_bearer_auth_headers(self.token) + + def tearDown(self): + """Clean up after each test. + + Resets storage for per-test isolation, ensuring tests don't pollute + each other's state. This eliminates the need for test_00_* prefixes. + """ + self.app_context.pop() + + # Reset storage for per-test isolation + if hasattr(self, 'service_manager'): + self.service_manager.reset_test_data() @classmethod def tearDownClass(cls): @@ -49,20 +64,8 @@ def tearDownClass(cls): import campus.storage.testing campus.storage.testing.reset_test_storage() - def setUp(self): - """Set up test environment before each test.""" - self.client = self.app.test_client() - - # Set up test context - self.app_context = self.app.app_context() - self.app_context.push() - - def tearDown(self): - """Clean up after each test.""" - self.app_context.pop() - - def test_00_list_assignments_empty(self): - """GET /assignments should return empty list initially. (Named test_00_* to run first for isolation)""" + def test_list_assignments_empty(self): + """GET /assignments should return empty list initially.""" response = self.client.get('/api/v1/assignments/', headers=self.auth_headers) assert response.status_code == 200