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

368 lines
9.6 KiB
Markdown

# 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:
```python
# 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:
```python
# 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:
```python
# 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):
```python
# 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:
```python
# 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
```bash
pytest
```
### Specific Test Types
```bash
# 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
```bash
# 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:
```python
# 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`:**
```python
# ✅ 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:**
```python
# ❌ 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`):
```python
@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:**
```python
# ✅ 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:**
```python
# ❌ 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/`):
```python
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:**
```python
# ✅ 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:**
```python
# DON'T DO THIS - breaks test isolation
@pytest.fixture
def session(test_db):
with Session(test_db) as session:
yield session
```
**❌ Using Hardcoded IDs:**
```python
# DON'T DO THIS - causes primary key conflicts
cardset = Cardset(id=1, name="Test")
team = Team(id=100, abbrev="TST")
```
**❌ Manual Model Creation:**
```python
# 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:**
```python
# 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