strat-gameplay-webapp/backend/tests/CLAUDE.md
Cal Corum 0b6076d5b8 CLAUDE: Implement Phase 3B - X-Check league config tables
Complete X-Check resolution table system for defensive play outcomes.

Components:
- Defense range tables (20×5) for infield, outfield, catcher
- Error charts for LF/RF and CF (ratings 0-25)
- Placeholder error charts for P, C, 1B, 2B, 3B, SS (awaiting data)
- get_fielders_holding_runners() - Complete implementation
- get_error_chart_for_position() - Maps all 9 positions
- 6 X-Check placeholder advancement functions (g1-g3, f1-f3)

League Config Integration:
- Both SbaConfig and PdConfig include X-Check tables
- Shared common tables via league_configs.py
- Attributes: x_check_defense_tables, x_check_error_charts, x_check_holding_runners

Testing:
- 36 tests for X-Check tables (all passing)
- 9 tests for X-Check placeholders (all passing)
- Total: 45/45 tests passing

Documentation:
- Updated backend/CLAUDE.md with Phase 3B section
- Updated app/config/CLAUDE.md with X-Check tables documentation
- Updated app/core/CLAUDE.md with X-Check placeholder functions
- Updated tests/CLAUDE.md with new test counts (519 unit tests)
- Updated phase-3b-league-config-tables.md (marked complete)
- Updated NEXT_SESSION.md with Phase 3B completion

What's Pending:
- 6 infield error charts need actual data (P, C, 1B, 2B, 3B, SS)
- Phase 3C will implement full X-Check resolution logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 19:50:55 -05:00

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

# 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

# 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

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

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

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

# 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


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.