Updated CLAUDE.md files with: - Current test counts and status - Session injection pattern documentation - New module references and architecture notes - Updated Phase status (3E-Final complete) - Enhanced troubleshooting guides Files updated: - Root CLAUDE.md: Project overview and phase status - backend/CLAUDE.md: Backend overview with test counts - backend/README.md: Quick start and development guide - backend/app/api/CLAUDE.md: API routes documentation - backend/app/database/CLAUDE.md: Session injection docs - backend/app/utils/CLAUDE.md: Utilities documentation - backend/tests/CLAUDE.md: Testing patterns and policy - frontend-sba/CLAUDE.md: Frontend overview - frontend-sba/store/CLAUDE.md: Store patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
16 KiB
Backend Tests - Developer Guide
Overview
Comprehensive test suite for the Paper Dynasty backend game engine covering unit tests, integration tests, and end-to-end scenarios.
Test Structure:
tests/
├── unit/ # Fast, isolated unit tests (no DB)
│ ├── config/ # League configs, PlayOutcome enum
│ ├── core/ # Game engine, dice, state manager, validators
│ ├── models/ # Pydantic models (game, player, roster)
│ └── terminal_client/ # Terminal client modules
├── integration/ # Database-dependent tests
│ ├── database/ # DatabaseOperations tests
│ ├── test_game_engine.py # Full game engine with DB
│ └── test_state_persistence.py # State recovery tests
└── e2e/ # End-to-end tests (future)
Testing Policy
🚨 REQUIRED: 100% unit tests passing before committing to any feature branch.
Commit Policy
This project enforces a strict testing policy to maintain code quality and prevent regressions.
Before Every Commit:
- ✅ MUST: Run
uv run pytest tests/unit/ -q - ✅ MUST: All 609 unit tests passing (100%)
- ✅ MUST: Fix any failing tests before committing
- ⚠️ OPTIONAL: Use
--no-verifyfor[WIP]commits (feature branches only)
Before Merging to Main:
- ✅ MUST: 100% unit tests passing
- ✅ MUST: Code review approval
- ✅ MUST: CI/CD green build
- ❌ NEVER: Merge with failing tests
Automated Enforcement:
A git pre-commit hook is available to automatically run tests before each commit.
# Install the hook (one-time setup)
cd /mnt/NV2/Development/strat-gameplay-webapp/backend
cp .git-hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
See backend/CLAUDE.md → "Testing Policy" section for full details.
Current Test Baseline
Must maintain or improve:
- ✅ Unit tests: 979/979 passing (100%)
- ✅ Integration tests: 32/32 passing (100%)
- ⏱️ Unit execution: ~4 seconds
- ⏱️ Integration execution: ~5 seconds
- 📊 Coverage: High coverage of core systems
Running Tests
Unit Tests (Recommended)
Fast, reliable, no database required:
# All unit tests
uv run pytest tests/unit/ -v
# Specific module
uv run pytest tests/unit/core/test_game_engine.py -v
# Specific test
uv run pytest tests/unit/core/test_game_engine.py::TestGameEngine::test_start_game -v
# With coverage
uv run pytest tests/unit/ --cov=app --cov-report=html
Unit tests should always pass. If they don't, it's a real code issue.
Integration Tests (Database Required)
✅ Integration tests now work reliably using session injection pattern.
# Run all integration tests
uv run pytest tests/integration/database/test_operations.py -v
# Run specific test class
uv run pytest tests/integration/database/test_operations.py::TestDatabaseOperationsGame -v
# Run with markers
uv run pytest tests/ -v -m integration
Session Injection Pattern (IMPORTANT)
Integration tests use session injection to avoid asyncpg connection conflicts:
# tests/integration/conftest.py
@pytest_asyncio.fixture(scope="function")
async def db_session():
"""Provide isolated database session with automatic rollback"""
async with TestAsyncSessionLocal() as session:
yield session
await session.rollback() # Cleanup after test
@pytest_asyncio.fixture(scope="function")
async def db_ops(db_session: AsyncSession):
"""DatabaseOperations with injected session"""
return DatabaseOperations(db_session) # All operations share this session
Key Points:
- Each test gets its own session (no connection conflicts)
- Session is rolled back after test (automatic cleanup)
- Tests use
NullPoolto prevent connection reuse issues - All
db_opsmethods use the same session (no "operation in progress" errors)
See app/database/CLAUDE.md for full session injection documentation.
All Tests
# Run everything
uv run pytest tests/ -v
# Run with markers
uv run pytest tests/ -v -m "not integration" # Skip integration tests
uv run pytest tests/ -v -m integration # Only integration tests
Test Configuration
pytest.ini
[pytest]
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
integration: marks tests that require database (deselect with '-m "not integration"')
Key Settings:
asyncio_mode = auto: Automatically detect async testsasyncio_default_fixture_loop_scope = function: Each test gets own event loop
Fixture Scopes
Critical for async tests:
# ✅ CORRECT - Matching scopes
@pytest.fixture(scope="function")
async def event_loop():
...
@pytest.fixture(scope="function")
async def db_session(event_loop):
...
# ❌ WRONG - Mismatched scopes cause errors
@pytest.fixture(scope="module") # Module scope
async def setup_database(event_loop): # But depends on function-scoped event_loop
...
Rule: Async fixtures should typically use scope="function" to avoid event loop conflicts.
Common Test Patterns
Unit Test Pattern
import pytest
from app.core.game_engine import GameEngine
from app.models.game_models import GameState
class TestGameEngine:
"""Unit tests for game engine - no database required"""
def test_something(self):
"""Test description"""
# Arrange
engine = GameEngine()
state = GameState(game_id=uuid4(), league_id="sba", ...)
# Act
result = engine.some_method(state)
# Assert
assert result.success is True
Integration Test Pattern (Session Injection)
import pytest
from uuid import uuid4
# Mark all tests in module as integration tests
pytestmark = pytest.mark.integration
class TestDatabaseOperations:
"""Integration tests - use fixtures from tests/integration/conftest.py"""
async def test_create_game(self, db_ops, db_session):
"""
Test creating a game in database.
Uses db_ops fixture (DatabaseOperations with injected session)
and db_session for flush/visibility within transaction.
"""
# Arrange
game_id = uuid4()
# Act
game = await db_ops.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
await db_session.flush() # Make visible within session
# Assert
retrieved = await db_ops.get_game(game_id)
assert retrieved is not None
assert retrieved.id == game_id
# Session automatically rolled back after test
Key Pattern Notes:
- Fixtures (
db_ops,db_session) come fromtests/integration/conftest.py - Use
await db_session.flush()to persist changes within test (notcommit()) - Session is automatically rolled back after each test (isolation)
- All operations share same session (no connection conflicts)
Async Test Pattern
import pytest
# pytest-asyncio automatically detects async tests
async def test_async_operation():
result = await some_async_function()
assert result is not None
# Or explicit marker (not required with asyncio_mode=auto)
@pytest.mark.asyncio
async def test_another_async_operation():
...
Database Testing
Test Database Setup
Required Environment Variables (.env):
DATABASE_URL=postgresql+asyncpg://paperdynasty:PASSWORD@10.10.0.42:5432/paperdynasty_dev
Database Connection:
- Integration tests use the same database as development
- Tests should clean up after themselves (fixtures handle this)
- Each test should create unique game IDs to avoid conflicts
Transaction Rollback Pattern (tests/integration/conftest.py)
from sqlalchemy import NullPool
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
# Use NullPool to prevent connection reuse issues in tests
test_engine = create_async_engine(DATABASE_URL, poolclass=NullPool)
TestAsyncSessionLocal = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
@pytest_asyncio.fixture(scope="function")
async def db_session():
"""Provide isolated database session with automatic rollback"""
async with TestAsyncSessionLocal() as session:
yield session
await session.rollback() # Cleanup - discard all changes
@pytest_asyncio.fixture(scope="function")
async def db_ops(db_session: AsyncSession):
"""DatabaseOperations with injected session for test isolation"""
return DatabaseOperations(db_session)
Why This Works:
NullPool: Prevents asyncpg connection reuse issuesscope="function": Each test gets fresh sessionrollback(): Automatically discards all test data- Session injection: All
db_opsmethods use same session (no conflicts)
Known Test Issues
All major test infrastructure issues have been resolved. The test suite is now stable.
Historical Context (Resolved)
AsyncPG Connection Conflicts - RESOLVED (2025-11-27)
- Was: "cannot perform operation: another operation is in progress" errors
- Fix: Session injection pattern in
DatabaseOperations+NullPoolin test fixtures - Result: 32/32 integration tests now passing
Test Coverage
Current Status (as of 2025-11-27):
- ✅ 979 unit tests passing (100%)
- ✅ 32 integration tests passing (100%)
- Total: 1,011 tests passing
Coverage by Module:
app/config/ ✅ Well covered
app/core/game_engine.py ✅ Well covered (unit + integration)
app/core/state_manager.py ✅ Well covered
app/core/dice.py ✅ Well covered
app/models/ ✅ Well covered
app/database/operations.py ✅ 32 integration tests (session injection pattern)
app/websocket/handlers.py ✅ 148 WebSocket handler tests
app/middleware/ ✅ Rate limiting, exceptions tested
Testing Best Practices
DO
- ✅ Write unit tests first (fast, reliable, no DB)
- ✅ Use descriptive test names:
test_game_ends_after_27_outs - ✅ Follow Arrange-Act-Assert pattern
- ✅ Use fixtures for common setup
- ✅ Test edge cases and error conditions
- ✅ Mock external dependencies (API calls, time, random)
- ✅ Keep tests independent (no shared state)
- ✅ Run unit tests frequently during development
DON'T
- ❌ Don't share state between tests
- ❌ Don't test implementation details (test behavior)
- ❌ Don't use real API calls in tests (use mocks)
- ❌ Don't depend on test execution order
- ❌ Don't use
commit()in integration tests (useflush()- session auto-rollbacks) - ❌ Don't create
DatabaseOperations()without session injection in integration tests
Mocking Examples
from unittest.mock import Mock, patch, AsyncMock
# Mock database operations
@patch('app.core.game_engine.DatabaseOperations')
def test_with_mock_db(mock_db_class):
mock_db = Mock()
mock_db_class.return_value = mock_db
mock_db.create_game = AsyncMock(return_value=None)
# Test code that uses DatabaseOperations
...
# Mock dice rolls for deterministic tests
@patch('app.core.dice.DiceSystem.roll_d20')
def test_with_fixed_roll(mock_roll):
mock_roll.return_value = 15
# Test code expecting roll of 15
...
# Mock Pendulum time
with time_machine.travel("2025-10-31 12:00:00", tick=False):
# Test time-dependent code
...
Debugging Failed Tests
Verbose Output
# Show full output including print statements
uv run pytest tests/unit/core/test_game_engine.py -v -s
# Show local variables on failure
uv run pytest tests/unit/core/test_game_engine.py -v -l
# Stop on first failure
uv run pytest tests/unit/core/test_game_engine.py -v -x
# Show full traceback
uv run pytest tests/unit/core/test_game_engine.py -v --tb=long
Interactive Debugging
# Drop into debugger on failure
uv run pytest tests/unit/core/test_game_engine.py --pdb
# Drop into debugger on first failure
uv run pytest tests/unit/core/test_game_engine.py --pdb -x
Logging in Tests
Tests capture logs by default. View with -o log_cli=true:
uv run pytest tests/unit/core/test_game_engine.py -v -o log_cli=true -o log_cli_level=DEBUG
CI/CD Considerations
Recommended CI Test Strategy:
# Run fast unit tests on every commit
- name: Unit Tests
run: uv run pytest tests/unit/ -v --cov=app
# Run integration tests (session injection pattern makes these reliable)
- name: Integration Tests
run: uv run pytest tests/integration/database/test_operations.py -v
All tests can now run together thanks to session injection pattern.
Troubleshooting
"cannot perform operation: another operation is in progress"
This issue is RESOLVED. If you see it, you're likely:
- Creating
DatabaseOperations()without session injection in tests - Not using the
db_opsfixture fromtests/integration/conftest.py
Solution: Use session injection pattern:
async def test_something(self, db_ops, db_session): # Use fixtures!
await db_ops.create_game(...) # Uses injected session
"Task got Future attached to a different loop"
Solution: Ensure all fixtures use scope="function" (already configured in conftest.py)
"No module named 'app'"
Solution:
# Recommended: Use UV (handles PYTHONPATH automatically)
cd /mnt/NV2/Development/strat-gameplay-webapp/backend
uv run pytest tests/unit/ -v
# Alternative: Set PYTHONPATH manually
export PYTHONPATH=/mnt/NV2/Development/strat-gameplay-webapp/backend
pytest tests/unit/ -v
Tests hang indefinitely
Likely Cause: Async test without proper event loop cleanup
Solution: Check fixture scopes and ensure asyncio_mode = auto in pytest.ini
Database connection errors
Check:
- PostgreSQL is running:
psql $DATABASE_URL .envhas correctDATABASE_URL- Database exists and schema is migrated:
alembic upgrade head
Integration Test Architecture (Completed 2025-11-27)
The integration test infrastructure has been fully refactored using session injection:
Key Components
-
tests/integration/conftest.py:NullPoolengine (prevents connection reuse issues)db_sessionfixture (function-scoped, auto-rollback)db_opsfixture (DatabaseOperationswith injected session)
-
app/database/operations.py:- Constructor accepts optional
AsyncSession _get_session()context manager handles both modes- Methods use
flush()notcommit()
- Constructor accepts optional
-
app/core/game_engine.py:- Creates
DatabaseOperations(session)for transaction groups - Ensures atomic save_play + update_game_state operations
- Creates
Pattern Summary
Production: db_ops = DatabaseOperations() → Auto-commit per operation
Testing: db_ops = DatabaseOperations(session) → Shared session, caller controls
Transactions: db_ops = DatabaseOperations(session) → Multiple ops, single commit
Additional Resources
- Database CLAUDE.md:
app/database/CLAUDE.md- Full session injection documentation - pytest-asyncio docs: https://pytest-asyncio.readthedocs.io/
- AsyncPG docs: https://magicstack.github.io/asyncpg/
- SQLAlchemy async: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
Summary: All 1,011 tests passing (979 unit + 32 integration). Session injection pattern ensures reliable, isolated integration tests with automatic cleanup.