- 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> |
||
|---|---|---|
| .. | ||
| e2e | ||
| factories | ||
| integration | ||
| unit | ||
| __init__.py | ||
| conftest.py | ||
| README.md | ||
| TEST_ISOLATION_GUIDE.md | ||
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
- Services: Core business logic with mocked dependencies
- Engine: Stateless game simulation functions
- Integration: Service interactions with real database
- 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-postgreson 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:
- Use
db_sessionfixture fromconftest.py- never create custom session fixtures - Use factory classes for all test data - never hardcode IDs or use static values
- Import factories from
tests.factoriespackage - 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:
- Extract and test business logic in service unit tests
- Validate game mechanics with engine tests
- Test service integration with database
- Ensure web interface works with e2e tests