strat-gameplay-webapp/backend/tests/CLAUDE.md
Cal Corum 440adf2c26 CLAUDE: Update REPL for new GameState and standardize UV commands
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>
2025-11-04 09:59:13 -06:00

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

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 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:

# 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:

  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

# 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 tests
  • asyncio_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()) 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-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:

  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


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.