Updated terminal client REPL to work with refactored GameState structure where current_batter/pitcher/catcher are now LineupPlayerState objects instead of integer IDs. Also standardized all documentation to properly show 'uv run' prefixes for Python commands. REPL Updates: - terminal_client/display.py: Access lineup_id from LineupPlayerState objects - terminal_client/repl.py: Fix typos (self.current_game → self.current_game_id) - tests/unit/terminal_client/test_commands.py: Create proper LineupPlayerState objects in test fixtures (2 tests fixed, all 105 terminal client tests passing) Documentation Updates (100+ command examples): - CLAUDE.md: Updated pytest examples to use 'uv run' prefix - terminal_client/CLAUDE.md: Updated ~40 command examples - tests/CLAUDE.md: Updated all test commands (unit, integration, debugging) - app/*/CLAUDE.md: Updated test and server startup commands (5 files) All Python commands now consistently use 'uv run' prefix to align with project's UV migration, improving developer experience and preventing confusion about virtual environment activation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
15 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)
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)
⚠️ 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 progressTask 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:
# Run one test at a time (always works)
uv run pytest tests/integration/database/test_operations.py::TestDatabaseOperationsGame::test_create_game -v
# Run test class serially
uv run pytest tests/integration/database/test_operations.py::TestDatabaseOperationsGame -v
# Run entire file (may have conflicts after first test)
uv run pytest tests/integration/database/test_operations.py -v
# Force serial execution (slower but more reliable)
uv run 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:
- Update fixtures to use proper asyncio scope management
- Ensure each test gets isolated database session
- Consider using
pytest-xdistwith proper worker isolation - Or redesign fixtures to create fresh connections per test
All Tests
# Run everything (expect integration failures due to connection issues)
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
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
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
@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()) expectpos_1field but test fixtures don't provide it - Attribute name mismatch: tests expect
player_idbut model hasid - Display name format mismatch in
PdPlayer.get_display_name()
Files to Fix:
app/models/player_models.py- Update abstract methods and factory methodstests/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-11-01):
- ✅ 519 unit tests passing (92% of unit tests)
- Added 45 new tests for Phase 3B (X-Check tables and placeholders)
- ❌ 14 unit tests failing (player models + 1 dice test - pre-existing)
- ❌ 49 integration test errors (connection conflicts - infrastructure issue)
- ❌ 28 integration test failures (various - pre-existing)
Coverage by Module:
app/config/ ✅ 94/94 tests passing (+36 X-Check table tests)
- test_league_configs.py 28 tests
- test_play_outcome.py 30 tests
- test_x_check_tables.py 36 tests (NEW - Phase 3B)
app/core/game_engine.py ✅ Well covered (unit tests)
app/core/runner_advancement.py ✅ 60/60 tests passing (+9 X-Check placeholders)
- test_runner_advancement.py 51 tests (groundball + placeholders)
- test_flyball_advancement.py 21 tests
app/core/state_manager.py ✅ 26/26 tests passing
app/core/dice.py ⚠️ 1 failure (roll history - pre-existing)
app/models/game_models.py ✅ 60/60 tests passing
app/models/player_models.py ❌ 13/32 tests failing (pre-existing)
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
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 serially (slower but reliable)
- name: Integration Tests
run: |
uv run pytest tests/integration/database/test_operations.py::TestDatabaseOperationsGame -v
uv run 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:
# 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 Refactor TODO
When refactoring integration tests to fix connection conflicts:
-
Update fixtures in
tests/integration/conftest.py:- Change all fixtures to
scope="function" - Ensure each test gets fresh database session
- Implement proper session cleanup
- Change all fixtures to
-
Add connection pooling:
- Consider using separate connection pool for tests
- Or create new engine per test (slower but isolated)
-
Add transaction rollback:
- Wrap each test in transaction
- Rollback after test completes
- Ensures database is clean for next test
-
Consider pytest-xdist:
- Run tests in parallel with proper worker isolation
- Each worker gets own database connection
- Faster test execution
-
Update
test_state_persistence.py:- Fix
setup_databasefixture scope mismatch - Consider splitting into smaller fixtures
- Fix
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.