- 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>
368 lines
9.6 KiB
Markdown
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 |