# 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 ### Unit Tests (Recommended) **Fast, reliable, no database required**: ```bash # 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**: ```bash # 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 ```bash # 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 ```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**: ```python # ✅ 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 ```python 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 ```python 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 ```python 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`): ```bash 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 ```python @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 ```python 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 ```bash # 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 ```bash # 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`: ```bash 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**: ```yaml # 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**: ```bash # 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 - **pytest-asyncio docs**: https://pytest-asyncio.readthedocs.io/ - **AsyncPG docs**: https://magicstack.github.io/asyncpg/ - **SQLAlchemy async**: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html - **Backend CLAUDE.md**: `../CLAUDE.md` - Main backend documentation --- **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.