## Refactoring - Changed `ManualOutcomeSubmission.outcome` from `str` to `PlayOutcome` enum type - Removed custom validator (Pydantic handles enum validation automatically) - Added direct import of PlayOutcome (no circular dependency due to TYPE_CHECKING guard) - Updated tests to use enum values while maintaining backward compatibility Benefits: - Better type safety with IDE autocomplete - Cleaner code (removed 15 lines of validator boilerplate) - Backward compatible (Pydantic auto-converts strings to enum) - Access to helper methods (is_hit(), is_out(), etc.) Files modified: - app/models/game_models.py: Enum type + import - tests/unit/config/test_result_charts.py: Updated 7 tests + added compatibility test ## Documentation Created comprehensive CLAUDE.md files for all backend/app/ subdirectories to help future AI agents quickly understand and work with the code. Added 8,799 lines of documentation covering: - api/ (906 lines): FastAPI routes, health checks, auth patterns - config/ (906 lines): League configs, PlayOutcome enum, result charts - core/ (1,288 lines): GameEngine, StateManager, PlayResolver, dice system - data/ (937 lines): API clients (planned), caching layer - database/ (945 lines): Async sessions, operations, recovery - models/ (1,270 lines): Pydantic/SQLAlchemy models, polymorphic patterns - utils/ (959 lines): Logging, JWT auth, security - websocket/ (1,588 lines): Socket.io handlers, real-time events - tests/ (475 lines): Testing patterns and structure Each CLAUDE.md includes: - Purpose & architecture overview - Key components with detailed explanations - Patterns & conventions - Integration points - Common tasks (step-by-step guides) - Troubleshooting with solutions - Working code examples - Testing guidance Total changes: +9,294 lines / -24 lines Tests: All passing (62/62 model tests, 7/7 ManualOutcomeSubmission tests)
476 lines
14 KiB
Markdown
476 lines
14 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)
|
|
```
|
|
|
|
## Running Tests
|
|
|
|
### Unit Tests (Recommended)
|
|
|
|
**Fast, reliable, no database required**:
|
|
```bash
|
|
# All unit tests
|
|
pytest tests/unit/ -v
|
|
|
|
# Specific module
|
|
pytest tests/unit/core/test_game_engine.py -v
|
|
|
|
# Specific test
|
|
pytest tests/unit/core/test_game_engine.py::TestGameEngine::test_start_game -v
|
|
|
|
# With coverage
|
|
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)
|
|
|
|
**⚠️ CRITICAL: Integration tests have known infrastructure issues**
|
|
|
|
#### Known Issue: AsyncPG Connection Conflicts
|
|
|
|
**Problem**: Integration tests share database connections and event loops, causing:
|
|
- `asyncpg.exceptions._base.InterfaceError: cannot perform operation: another operation is in progress`
|
|
- `Task got Future attached to a different loop`
|
|
|
|
**Why This Happens**:
|
|
- AsyncPG connections don't support concurrent operations
|
|
- pytest-asyncio fixtures with mismatched scopes (module vs function)
|
|
- Tests running in parallel try to reuse the same connection
|
|
|
|
**Current Workaround**: Run integration tests **individually** or **serially**:
|
|
|
|
```bash
|
|
# Run one test at a time (always works)
|
|
pytest tests/integration/database/test_operations.py::TestDatabaseOperationsGame::test_create_game -v
|
|
|
|
# Run test class serially
|
|
pytest tests/integration/database/test_operations.py::TestDatabaseOperationsGame -v
|
|
|
|
# Run entire file (may have conflicts after first test)
|
|
pytest tests/integration/database/test_operations.py -v
|
|
|
|
# Force serial execution (slower but more reliable)
|
|
pytest tests/integration/ -v -x # -x stops on first failure
|
|
```
|
|
|
|
**DO NOT**:
|
|
- Run all integration tests at once: `pytest tests/integration/ -v` ❌ (will fail)
|
|
- Expect integration tests to work in parallel ❌
|
|
|
|
**Long-term Fix Needed**:
|
|
1. Update fixtures to use proper asyncio scope management
|
|
2. Ensure each test gets isolated database session
|
|
3. Consider using `pytest-xdist` with proper worker isolation
|
|
4. Or redesign fixtures to create fresh connections per test
|
|
|
|
### All Tests
|
|
|
|
```bash
|
|
# Run everything (expect integration failures due to connection issues)
|
|
pytest tests/ -v
|
|
|
|
# Run with markers
|
|
pytest tests/ -v -m "not integration" # Skip integration tests
|
|
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
|
|
|
|
```python
|
|
import pytest
|
|
from app.database.operations import DatabaseOperations
|
|
from app.database.session import AsyncSessionLocal
|
|
|
|
@pytest.mark.integration
|
|
class TestDatabaseOperations:
|
|
"""Integration tests - requires database"""
|
|
|
|
@pytest.fixture
|
|
async def db_ops(self):
|
|
"""Create DatabaseOperations instance"""
|
|
ops = DatabaseOperations()
|
|
yield ops
|
|
|
|
async def test_create_game(self, db_ops):
|
|
"""Test description"""
|
|
# Arrange
|
|
game_id = uuid4()
|
|
|
|
# Act
|
|
await db_ops.create_game(game_id=game_id, ...)
|
|
|
|
# Assert
|
|
game = await db_ops.get_game(game_id)
|
|
assert game is not None
|
|
```
|
|
|
|
### 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
|
|
|
|
```python
|
|
@pytest.fixture
|
|
async def db_session():
|
|
"""Provide isolated database session with automatic rollback"""
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
yield session
|
|
# Automatic rollback on fixture teardown
|
|
```
|
|
|
|
**Note**: Current fixtures may not properly isolate transactions, contributing to connection conflicts.
|
|
|
|
## Known Test Issues
|
|
|
|
### 1. Player Model Test Failures
|
|
|
|
**Issue**: `tests/unit/models/test_player_models.py` has 13 failures
|
|
|
|
**Root Causes**:
|
|
- `BasePlayer.get_image_url()` not marked as `@abstractmethod`
|
|
- Factory methods (`from_api_response()`) expect `pos_1` field but test fixtures don't provide it
|
|
- Attribute name mismatch: tests expect `player_id` but model has `id`
|
|
- Display name format mismatch in `PdPlayer.get_display_name()`
|
|
|
|
**Files to Fix**:
|
|
- `app/models/player_models.py` - Update abstract methods and factory methods
|
|
- `tests/unit/models/test_player_models.py` - Update test fixtures to match API response format
|
|
|
|
### 2. Dice System Test Failure
|
|
|
|
**Issue**: `tests/unit/core/test_dice.py::TestRollHistory::test_get_rolls_since`
|
|
|
|
**Symptom**: Expects 1 roll but gets 0
|
|
|
|
**Likely Cause**: Roll history not properly persisting or timestamp filtering issue
|
|
|
|
### 3. Integration Test Connection Conflicts
|
|
|
|
**Issue**: 49 integration test errors due to AsyncPG connection conflicts
|
|
|
|
**Status**: **Known infrastructure issue** - not code bugs
|
|
|
|
**When It Matters**: Only when running multiple integration tests in sequence
|
|
|
|
**When It Doesn't**: Unit tests (474 passing) validate business logic
|
|
|
|
### 4. Event Loop Scope Mismatch
|
|
|
|
**Issue**: `tests/integration/test_state_persistence.py` - all 7 tests fail with scope mismatch
|
|
|
|
**Error**: `ScopeMismatch: You tried to access the function scoped fixture event_loop with a module scoped request object`
|
|
|
|
**Root Cause**: `setup_database` fixture is `scope="module"` but depends on `event_loop` which is `scope="function"`
|
|
|
|
**Fix**: Change `setup_database` to `scope="function"` or create module-scoped event loop
|
|
|
|
## Test Coverage
|
|
|
|
**Current Status** (as of 2025-10-31):
|
|
- ✅ **474 unit tests passing** (91% of unit tests)
|
|
- ❌ **14 unit tests failing** (player models + 1 dice test)
|
|
- ❌ **49 integration test errors** (connection conflicts)
|
|
- ❌ **28 integration test failures** (various)
|
|
|
|
**Coverage by Module**:
|
|
```
|
|
app/config/ ✅ 58/58 tests passing
|
|
app/core/game_engine.py ✅ Well covered (unit tests)
|
|
app/core/state_manager.py ✅ 26/26 tests passing
|
|
app/core/dice.py ⚠️ 1 failure (roll history)
|
|
app/models/game_models.py ✅ 60/60 tests passing
|
|
app/models/player_models.py ❌ 13/32 tests failing
|
|
app/database/operations.py ⚠️ Integration tests have infrastructure issues
|
|
```
|
|
|
|
## 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 run all integration tests at once (connection conflicts)
|
|
- ❌ 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 skip writing tests because integration tests are flaky
|
|
|
|
### 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
|
|
pytest tests/unit/core/test_game_engine.py -v -s
|
|
|
|
# Show local variables on failure
|
|
pytest tests/unit/core/test_game_engine.py -v -l
|
|
|
|
# Stop on first failure
|
|
pytest tests/unit/core/test_game_engine.py -v -x
|
|
|
|
# Show full traceback
|
|
pytest tests/unit/core/test_game_engine.py -v --tb=long
|
|
```
|
|
|
|
### Interactive Debugging
|
|
|
|
```bash
|
|
# Drop into debugger on failure
|
|
pytest tests/unit/core/test_game_engine.py --pdb
|
|
|
|
# Drop into debugger on first failure
|
|
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
|
|
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: pytest tests/unit/ -v --cov=app
|
|
|
|
# Run integration tests serially (slower but reliable)
|
|
- name: Integration Tests
|
|
run: |
|
|
pytest tests/integration/database/test_operations.py::TestDatabaseOperationsGame -v
|
|
pytest tests/integration/database/test_operations.py::TestDatabaseOperationsLineup -v
|
|
# ... run each test class separately
|
|
```
|
|
|
|
**OR** fix the integration test infrastructure first, then run normally.
|
|
|
|
## Troubleshooting
|
|
|
|
### "cannot perform operation: another operation is in progress"
|
|
|
|
**Solution**: Run integration tests individually or fix fixture scopes
|
|
|
|
### "Task got Future attached to a different loop"
|
|
|
|
**Solution**: Ensure all fixtures use `scope="function"` or create proper module-scoped event loop
|
|
|
|
### "No module named 'app'"
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Set PYTHONPATH
|
|
export PYTHONPATH=/mnt/NV2/Development/strat-gameplay-webapp/backend
|
|
|
|
# Or run from backend directory
|
|
cd /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 Refactor TODO
|
|
|
|
When refactoring integration tests to fix connection conflicts:
|
|
|
|
1. **Update fixtures in `tests/integration/conftest.py`**:
|
|
- Change all fixtures to `scope="function"`
|
|
- Ensure each test gets fresh database session
|
|
- Implement proper session cleanup
|
|
|
|
2. **Add connection pooling**:
|
|
- Consider using separate connection pool for tests
|
|
- Or create new engine per test (slower but isolated)
|
|
|
|
3. **Add transaction rollback**:
|
|
- Wrap each test in transaction
|
|
- Rollback after test completes
|
|
- Ensures database is clean for next test
|
|
|
|
4. **Consider pytest-xdist**:
|
|
- Run tests in parallel with proper worker isolation
|
|
- Each worker gets own database connection
|
|
- Faster test execution
|
|
|
|
5. **Update `test_state_persistence.py`**:
|
|
- Fix `setup_database` fixture scope mismatch
|
|
- Consider splitting into smaller fixtures
|
|
|
|
## Additional Resources
|
|
|
|
- **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
|
|
- **Backend CLAUDE.md**: `../CLAUDE.md` - Main backend documentation
|
|
|
|
---
|
|
|
|
**Summary**: Unit tests are solid (91% passing), integration tests have known infrastructure issues that need fixture refactoring. Focus on unit tests for development, fix integration test infrastructure as separate task.
|