paper-dynasty-gameplay-webapp/tests/README.md
Cal Corum 1c24161e76 CLAUDE: Achieve 100% test pass rate with comprehensive AI service testing
- Fix TypeError in check_steal_opportunity by properly mocking catcher defense
- Correct tag_from_third test calculation to account for all adjustment conditions
- Fix pitcher replacement test by setting appropriate allowed runners threshold
- Add comprehensive test coverage for AI service business logic
- Implement VS Code testing panel configuration with pytest integration
- Create pytest.ini for consistent test execution and warning management
- Add test isolation guidelines and factory pattern implementation
- Establish 102 passing tests with zero failures

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-28 17:55:34 -05:00

9.6 KiB

Tests Directory

This directory contains the comprehensive test suite for the Paper Dynasty web app, organized by testing scope and purpose following the Model/Service Architecture.

Testing Strategy

Test Pyramid Structure

  • Unit Tests (80%+ coverage): Fast, isolated tests for services and engine
  • Integration Tests (70%+ coverage): Service + database interactions
  • End-to-End Tests (Happy path): Complete user workflows

Testing Priorities

  1. Services: Core business logic with mocked dependencies
  2. Engine: Stateless game simulation functions
  3. Integration: Service interactions with real database
  4. Routes: HTTP handling with mocked services

Directory Structure

tests/
├── unit/                   # Fast, isolated unit tests
│   ├── services/           # Service unit tests (MOST IMPORTANT)
│   ├── engine/             # Engine unit tests
│   └── models/             # Model validation tests
├── integration/            # Service + database integration
└── e2e/                   # Full application tests

Unit Tests (unit/)

Services (unit/services/)

Critical for Model/Service Architecture

Tests business logic independently of database and web framework:

# test_game_service.py
@pytest.fixture
def mock_session():
    return Mock(spec=Session)

@pytest.fixture
def game_service(mock_session):
    return GameService(mock_session)

@pytest.mark.asyncio
async def test_create_game_validates_teams(game_service):
    # Test business logic without database
    with pytest.raises(ValidationError):
        await game_service.create_game(999, 1000)  # Invalid teams

Engine (unit/engine/)

Tests stateless game simulation functions:

# test_dice.py
def test_dice_probability_distribution():
    # Test dice rolling mechanics
    results = [roll_dice() for _ in range(1000)]
    assert 1 <= min(results) <= max(results) <= 6

# test_simulation.py
def test_pitcher_vs_batter_mechanics():
    # Test core game simulation
    result = simulate_at_bat(pitcher_stats, batter_stats)
    assert result.outcome in ['hit', 'out', 'walk', 'strikeout']

Models (unit/models/)

Tests data validation and relationships:

# test_models.py
def test_game_model_validation():
    # Test SQLModel validation
    with pytest.raises(ValidationError):
        Game(away_team_id=None)  # Required field

Integration Tests (integration/)

Tests service interactions with real database (isolated transactions):

# test_game_flow.py
@pytest.mark.asyncio
async def test_complete_game_creation_flow(db_session):
    game_service = GameService(db_session)

    # Create test data
    team1 = await create_test_team(db_session)
    team2 = await create_test_team(db_session)

    # Test full flow with real database
    game = await game_service.create_game(team1.id, team2.id)

    assert game.id is not None
    assert game.away_team_id == team1.id

End-to-End Tests (e2e/)

Tests complete user journeys through web interface:

# test_game_creation.py
def test_user_can_create_game(client):
    # Test complete user workflow
    response = client.post("/auth/login")  # Login
    response = client.post("/games/start", json={...})  # Create game
    response = client.get(f"/game/{game_id}")  # View game

    assert "Game created successfully" in response.text

Running Tests

All Tests

pytest

Specific Test Types

# Unit tests only (fast)
pytest tests/unit/

# Integration tests only
pytest tests/integration/

# End-to-end tests only
pytest tests/e2e/

# Specific service tests
pytest tests/unit/services/test_game_service.py

# Single test function
pytest tests/unit/services/test_game_service.py::test_create_game_success

With Coverage

# Coverage report
pytest --cov=app

# Coverage with HTML report
pytest --cov=app --cov-report=html

Test Configuration

Fixtures

Common test fixtures for database, services, and test data:

# conftest.py
@pytest.fixture
def db_session():
    # Isolated database session for integration tests
    pass

@pytest.fixture
def mock_game_service():
    # Mocked service for route testing
    pass

@pytest.fixture
def test_game_data():
    # Sample game data for tests
    pass

Test Database

Integration tests use a separate PostgreSQL test database:

  • Container: pdtest-postgres on port 5434 (via docker-compose)
  • URL: postgresql://paper_dynasty_user:paper_dynasty_test_password@localhost:5434/paper_dynasty_test
  • Isolation: Transaction rollback after each test
  • Clean state: Each test runs in isolation

Database Testing Strategy

🚨 CRITICAL: Always Use Centralized Fixtures

CORRECT - Use centralized db_session fixture from conftest.py:

# ✅ Good - uses proper rollback
def test_create_team(db_session):
    team = TeamFactory.create(db_session, name="Test Team")
    assert team.id is not None

WRONG - Never create custom database fixtures:

# ❌ BAD - creates data persistence issues
@pytest.fixture
def session(test_db):
    with Session(test_db) as session:
        yield session  # No rollback!

Transaction Rollback Pattern (Already implemented in conftest.py):

@pytest.fixture
def db_session(test_engine):
    """Database session with transaction rollback for test isolation."""
    connection = test_engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)

    try:
        yield session
    finally:
        session.close()
        transaction.rollback()  # ✅ Automatic cleanup
        connection.close()

Benefits:

  • Complete test isolation
  • Fast execution (no actual database writes)
  • No cleanup required
  • Deterministic test results

🚨 CRITICAL: Always Use Test Factories

CORRECT - Use factories with unique IDs:

# ✅ Good - uses factory with unique ID generation
def test_create_cardset(db_session):
    cardset = CardsetFactory.create(db_session, name="Test Set")
    assert cardset.id is not None

WRONG - Never use hardcoded IDs:

# ❌ BAD - hardcoded IDs cause conflicts
def test_create_cardset(db_session):
    cardset = Cardset(id=1, name="Test Set")  # Will conflict!
    db_session.add(cardset)
    db_session.commit()

Factory Pattern (see tests/factories/):

class CardsetFactory:
    @staticmethod
    def build(**kwargs):
        defaults = {
            'id': generate_unique_id(),  # ✅ Unique every time
            'name': generate_unique_name('Cardset'),
            'ranked_legal': False
        }
        defaults.update(kwargs)
        return Cardset(**defaults)

    @staticmethod
    def create(session, **kwargs):
        cardset = CardsetFactory.build(**kwargs)
        session.add(cardset)
        session.commit()
        session.refresh(cardset)
        return cardset

Benefits:

  • Unique data per test
  • No ID conflicts
  • Customizable test data
  • Readable test code

Testing Best Practices

Service Testing

  • Mock database sessions for unit tests
  • Test business logic independently of framework
  • Validate error handling and edge cases
  • Test service interactions with integration tests

Test Data Management

  • Use factories for creating test data
  • Isolate test state (no shared mutable state)
  • Clean up after tests (database rollback)

🚨 Test Isolation Requirements

MANDATORY for all new tests:

  1. Use db_session fixture from conftest.py - never create custom session fixtures
  2. Use factory classes for all test data - never hardcode IDs or use static values
  3. Import factories from tests.factories package
  4. Test in isolation - each test should work independently

Checklist for New Tests:

# ✅ Required imports
from tests.factories.team_factory import TeamFactory

# ✅ Required fixture usage
def test_something(db_session):  # Use db_session, not session
    pass

# ✅ Required factory usage
team = TeamFactory.create(db_session, name="Custom Name")
# NOT: team = Team(id=1, name="Custom Name")

# ✅ Required test isolation
# Each test should be runnable independently and repeatedly

🚨 Common Anti-Patterns to Avoid

Creating Custom Database Fixtures:

# DON'T DO THIS - breaks test isolation
@pytest.fixture
def session(test_db):
    with Session(test_db) as session:
        yield session

Using Hardcoded IDs:

# DON'T DO THIS - causes primary key conflicts
cardset = Cardset(id=1, name="Test")
team = Team(id=100, abbrev="TST")

Manual Model Creation:

# DON'T DO THIS - creates duplicate and brittle tests
def test_something(db_session):
    cardset = Cardset(
        id=generate_unique_id(),
        name="Manual Cardset",
        ranked_legal=False
    )
    db_session.add(cardset)
    db_session.commit()

Correct Patterns:

# DO THIS - uses proper isolation and factories
def test_something(db_session):
    cardset = CardsetFactory.create(
        db_session,
        name="Test Cardset",
        ranked_legal=False
    )
    # Test logic here

Coverage Goals

  • Services: 90%+ coverage (core business logic)
  • Engine: 95%+ coverage (critical game mechanics)
  • Routes: 80%+ coverage (HTTP handling)
  • Models: 85%+ coverage (data validation)

Migration Testing

When migrating from Discord app:

  1. Extract and test business logic in service unit tests
  2. Validate game mechanics with engine tests
  3. Test service integration with database
  4. Ensure web interface works with e2e tests