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>
524 lines
16 KiB
Markdown
524 lines
16 KiB
Markdown
# 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-verify` for `[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.
|
|
|
|
```bash
|
|
# 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**:
|
|
```bash
|
|
# 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.
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```python
|
|
# 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 `NullPool` to prevent connection reuse issues
|
|
- All `db_ops` methods use the same session (no "operation in progress" errors)
|
|
|
|
See `app/database/CLAUDE.md` for full session injection documentation.
|
|
|
|
### All Tests
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```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 tests
|
|
- `asyncio_default_fixture_loop_scope = function`: Each test gets own event loop
|
|
|
|
### Fixture Scopes
|
|
|
|
**Critical for async tests**:
|
|
|
|
```python
|
|
# ✅ 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
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
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 from `tests/integration/conftest.py`
|
|
- Use `await db_session.flush()` to persist changes within test (not `commit()`)
|
|
- Session is automatically rolled back after each test (isolation)
|
|
- All operations share same session (no connection conflicts)
|
|
|
|
### Async Test Pattern
|
|
|
|
```python
|
|
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`):
|
|
```bash
|
|
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)
|
|
|
|
```python
|
|
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 issues
|
|
- `scope="function"`: Each test gets fresh session
|
|
- `rollback()`: Automatically discards all test data
|
|
- Session injection: All `db_ops` methods 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` + `NullPool` in 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 (use `flush()` - session auto-rollbacks)
|
|
- ❌ Don't create `DatabaseOperations()` without session injection in integration tests
|
|
|
|
### Mocking Examples
|
|
|
|
```python
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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`:
|
|
|
|
```bash
|
|
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**:
|
|
|
|
```yaml
|
|
# 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_ops` fixture from `tests/integration/conftest.py`
|
|
|
|
**Solution**: Use session injection pattern:
|
|
```python
|
|
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**:
|
|
```bash
|
|
# 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**:
|
|
1. PostgreSQL is running: `psql $DATABASE_URL`
|
|
2. `.env` has correct `DATABASE_URL`
|
|
3. 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
|
|
|
|
1. **`tests/integration/conftest.py`**:
|
|
- `NullPool` engine (prevents connection reuse issues)
|
|
- `db_session` fixture (function-scoped, auto-rollback)
|
|
- `db_ops` fixture (`DatabaseOperations` with injected session)
|
|
|
|
2. **`app/database/operations.py`**:
|
|
- Constructor accepts optional `AsyncSession`
|
|
- `_get_session()` context manager handles both modes
|
|
- Methods use `flush()` not `commit()`
|
|
|
|
3. **`app/core/game_engine.py`**:
|
|
- Creates `DatabaseOperations(session)` for transaction groups
|
|
- Ensures atomic save_play + update_game_state operations
|
|
|
|
### 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.
|