6.1 KiB
Backend Test Optimization Plan
Overview
This plan implements three optimizations to speed up backend test execution:
- Session-scoped role setup (#4)
- Session-scoped schema + transaction rollback (#1)
- Parallel test execution (#2)
Current baseline: 236 tests in ~110 seconds (~0.46s per test)
Implementation Steps
Step 1: Session-Scoped Role Setup (#4)
Goal: Create roles once per test session instead of 236 times.
Changes:
- Create
@pytest.fixture(scope="session")for engine - Create
@pytest.fixture(scope="session")for roles setup - Modify
client_factoryto use pre-created roles instead of callingsetup_roles()each time
Benefits:
- Eliminates 236 role creation operations
- Roles are static data, safe to share across tests
Risks: Low - roles are read-only after creation
Step 2: Session-Scoped Schema Creation (#1)
Goal: Create database schema once per session instead of dropping/recreating 236 times.
Changes:
- Move schema creation (
drop_all+create_all) to session-scopedenginefixture - Schema created once at session start, cleaned up at session end
- Each test still gets a fresh database state via transaction rollback
Benefits:
- Eliminates 236 schema drop/create operations (major bottleneck)
- Expected 40-60% speed improvement
Risks: Medium - need to ensure proper cleanup and isolation
Step 3: Transaction Rollback Pattern (#1)
Goal: Use database transactions to isolate tests instead of dropping tables.
Approach:
- Each test runs inside a transaction
- After test completes, rollback the transaction (not commit)
- Next test starts with clean state automatically
Implementation Strategy:
- Create a session-scoped connection pool
- For each test:
- Start a transaction (or use a savepoint)
- Run test with all DB operations in this transaction
- Rollback transaction after test
- Override
get_db()to yield sessions within the transaction context
Key Challenge: FastAPI's get_db dependency needs to work with transaction boundaries.
Solution Options:
- Option A: Use nested transactions (savepoints) - more complex but better isolation
- Option B: Use connection-level transactions - simpler, rollback entire connection state
Recommended: Option B (simpler, sufficient for test isolation)
Changes:
- Modify
client_factoryto use transaction-scoped sessions - Update
get_db_session()to work within transaction context - Ensure all test DB operations happen within transaction
Benefits:
- Fast test isolation (rollback is much faster than drop/create)
- Maintains test independence
Risks: Medium - need to ensure:
- No commits happen during tests (or they're rolled back)
- Transaction boundaries are properly managed
- Async context managers work correctly
Step 4: Update Fixtures for New Architecture
Changes:
- Update
client_factoryto depend on session-scopedengineandroles - Update
get_db_session()to work with transaction rollback - Ensure user fixtures (
regular_user,admin_user, etc.) work with new pattern - Update
override_get_db()to yield sessions within transaction context
Testing: Run a subset of tests to verify fixtures work correctly
Step 5: Add pytest-xdist for Parallel Execution (#2)
Goal: Run tests in parallel across CPU cores.
Changes:
- Add
pytest-xdisttopyproject.tomldev dependencies - Update
Makefileto usepytest -n autofor parallel execution - Ensure test isolation is maintained (transaction rollback ensures this)
Configuration:
- Use
-n autoto auto-detect CPU cores - Can override with
-n 4for specific core count - Add
pytest-xdistto dependency groups
Benefits:
- 2-4x speed improvement (depending on CPU cores)
- Works well with transaction isolation
Risks: Low - transaction rollback ensures tests don't interfere
Note: May need to adjust if tests have shared state (but transaction rollback should prevent this)
Step 6: Testing and Validation
Verification Steps:
- Run full test suite:
make test-backend - Verify all 236 tests pass
- Measure execution time improvement
- Check for any flaky tests (shouldn't happen with proper isolation)
- Test parallel execution with
pytest -n auto
Success Criteria:
- All tests pass
- Significant speed improvement (target: 50-70% faster)
- No test flakiness introduced
- Parallel execution works correctly
Implementation Order
- ✅ Step 1: Session-scoped role setup (easiest, low risk)
- ✅ Step 2: Session-scoped schema creation (foundation for #3)
- ✅ Step 3: Transaction rollback pattern (core optimization)
- ✅ Step 4: Update all fixtures (required for #3 to work)
- ✅ Step 5: Add pytest-xdist (quick win, independent)
- ✅ Step 6: Test and validate
Technical Details
Transaction Rollback Pattern
# Pseudo-code for transaction pattern
@pytest.fixture(scope="function")
async def db_transaction(engine):
async with engine.connect() as conn:
trans = await conn.begin()
try:
# Create session factory that uses this connection
session_factory = async_sessionmaker(bind=conn, ...)
yield session_factory
finally:
await trans.rollback() # Always rollback, never commit
Session-Scoped Engine
@pytest.fixture(scope="session")
async def engine():
engine = create_async_engine(TEST_DATABASE_URL)
# Create schema once
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
yield engine
await engine.dispose()
Role Setup
@pytest.fixture(scope="session")
async def roles(engine):
session_factory = async_sessionmaker(engine)
async with session_factory() as db:
roles = await setup_roles(db)
await db.commit() # Commit roles once
return roles
Rollback Plan
If issues arise:
- Revert
conftest.pychanges - Remove
pytest-xdistdependency - Restore original fixture structure
All changes are isolated to test files, no production code affected.