# Testing Strategy for Player Models **Purpose**: Comprehensive test plan for Week 6 player model implementation --- ## Overview Testing will be organized into three layers: 1. **Unit Tests** - Individual components in isolation 2. **Integration Tests** - Components working together 3. **End-to-End Tests** - Full pipeline from API to game Target: **90%+ code coverage** on all new modules --- ## Unit Tests ### 1. API Models (`test_api_models.py`) **File**: `tests/unit/models/test_api_models.py` **Coverage**: Pydantic model deserialization with real JSON samples ```python import pytest from pydantic import ValidationError from app.models.api_models import ( SbaPlayerApi, PdPlayerApi, PdBattingRatingsResponseApi, PdPitchingRatingsResponseApi ) class TestSbaPlayerApi: """Test SBA API model deserialization""" def test_deserialize_sba_player(self, sba_player_json): """Test valid SBA player response""" player = SbaPlayerApi(**sba_player_json) assert player.id == 12288 assert player.name == "Ronald Acuna Jr" assert player.team.lname == "West Virginia Black Bears" assert player.team.manager1.name == "Cal" assert player.team.division.division_name == "Big Chungus" def test_sba_player_positions(self, sba_player_json): """Test position fields""" player = SbaPlayerApi(**sba_player_json) assert player.pos_1 == "RF" assert player.pos_2 is None def test_sba_player_missing_required_field(self, sba_player_json): """Test validation fails on missing required field""" del sba_player_json["name"] with pytest.raises(ValidationError) as exc_info: SbaPlayerApi(**sba_player_json) assert "name" in str(exc_info.value) class TestPdPlayerApi: """Test PD API model deserialization""" def test_deserialize_pd_player(self, pd_player_json): """Test valid PD player response""" player = PdPlayerApi(**pd_player_json) assert player.player_id == 11223 assert player.p_name == "Matt Karchner" assert player.cardset.name == "1998 Season" assert player.rarity.name == "MVP" assert player.mlbplayer.first_name == "Matt" def test_pd_player_positions(self, pd_player_json): """Test position fields""" player = PdPlayerApi(**pd_player_json) assert player.pos_1 == "RP" assert player.pos_2 == "CP" assert player.pos_3 is None class TestPdBattingRatingsApi: """Test PD batting ratings deserialization""" def test_deserialize_batting_ratings(self, pd_batting_ratings_json): """Test valid batting ratings response""" ratings = PdBattingRatingsResponseApi(**pd_batting_ratings_json) assert ratings.count == 2 assert len(ratings.ratings) == 2 # Check vs L rating vs_l = next(r for r in ratings.ratings if r.vs_hand == "L") assert vs_l.homerun == 0.0 assert vs_l.walk == 18.25 assert vs_l.strikeout == 9.75 # Check vs R rating vs_r = next(r for r in ratings.ratings if r.vs_hand == "R") assert vs_r.homerun == 1.05 assert vs_r.walk == 12.1 def test_batting_probabilities_sum_to_100(self, pd_batting_ratings_json): """Test that all probabilities sum to ~100""" ratings = PdBattingRatingsResponseApi(**pd_batting_ratings_json) for rating in ratings.ratings: total = ( rating.homerun + rating.bp_homerun + rating.triple + rating.double_three + rating.double_two + rating.double_pull + rating.single_two + rating.single_one + rating.single_center + rating.bp_single + rating.hbp + rating.walk + rating.strikeout + rating.lineout + rating.popout + rating.flyout_a + rating.flyout_bq + rating.flyout_lf_b + rating.flyout_rf_b + rating.groundout_a + rating.groundout_b + rating.groundout_c ) # Allow small float rounding errors assert 99.0 <= total <= 101.0, f"Total probability: {total}" class TestPdPitchingRatingsApi: """Test PD pitching ratings deserialization""" def test_deserialize_pitching_ratings(self, pd_pitching_ratings_json): """Test valid pitching ratings response""" ratings = PdPitchingRatingsResponseApi(**pd_pitching_ratings_json) assert ratings.count == 2 assert len(ratings.ratings) == 2 # Verify xcheck fields exist vs_l = ratings.ratings[0] assert vs_l.xcheck_p == 1.0 assert vs_l.xcheck_ss == 7.0 ``` --- ### 2. Game Models (`test_player_models.py`) **File**: `tests/unit/models/test_player_models.py` **Coverage**: Game model creation and methods ```python import pytest from pydantic import ValidationError from app.models.player_models import ( SbaPlayer, PdPlayer, PdBattingRating, PdPitchingRating ) class TestSbaPlayer: """Test SBA game player model""" def test_create_sba_player(self): """Test creating SBA player""" player = SbaPlayer( player_id=12288, name="Ronald Acuna Jr", positions=["RF", "CF"], image_url="https://example.com/image.png", team_name="West Virginia Black Bears", team_abbrev="WV" ) assert player.player_id == 12288 assert player.get_primary_position() == "RF" assert player.can_play_position("CF") assert not player.can_play_position("P") def test_sba_player_display_name(self): """Test display name formatting""" player = SbaPlayer( player_id=1, name="Test Player", positions=["SS"], image_url="https://example.com/image.png", team_name="Test Team", team_abbrev="TT" ) assert player.get_display_name() == "Test Player (SS)" def test_sba_player_requires_positions(self): """Test validation fails without positions""" # Empty positions list should be invalid with pytest.raises(ValidationError): SbaPlayer( player_id=1, name="Test", positions=[], image_url="url", team_name="Team", team_abbrev="TM" ) class TestPdPlayer: """Test PD game player model""" def test_create_pd_player_with_batting(self): """Test creating PD position player""" batting_vs_lhp = PdBattingRating( vs_hand="L", homerun=2.0, triple=1.0, double=10.0, single=15.0, walk=12.0, hbp=2.0, strikeout=10.0, flyout=5.0, lineout=8.0, popout=15.0, groundout=20.0, bp_homerun=3.0, bp_single=5.0, avg=0.250, obp=0.350, slg=0.450 ) player = PdPlayer( player_id=10633, name="Chuck Knoblauch", positions=["2B"], image_url="https://example.com/image.png", team_name="New York Yankees", cardset_id=20, cardset_name="1998 Season", is_pitcher=False, hand="R", batting_vs_lhp=batting_vs_lhp, batting_vs_rhp=None ) assert player.player_id == 10633 assert not player.is_pitcher assert player.get_batting_rating("L") == batting_vs_lhp assert player.get_pitching_rating("L") is None def test_pd_player_display_name_with_handedness(self): """Test display name includes handedness symbol""" player = PdPlayer( player_id=1, name="Test Player", positions=["2B"], image_url="url", team_name="Team", cardset_id=1, cardset_name="2021", is_pitcher=False, hand="L" ) display = player.get_display_name() assert "⟨" in display # Left-handed symbol assert "(2B)" in display class TestPdBattingRating: """Test PD batting rating model""" def test_total_probability_calculation(self): """Test probability sum calculation""" rating = PdBattingRating( vs_hand="L", homerun=2.0, triple=1.0, double=10.0, single=15.0, walk=12.0, hbp=2.0, strikeout=10.0, flyout=5.0, lineout=8.0, popout=15.0, groundout=20.0, bp_homerun=3.0, bp_single=5.0, avg=0.250, obp=0.350, slg=0.450 ) total = rating.total_probability() assert total == 108.0 # Sum of all outcomes ``` --- ### 3. Mappers (`test_mappers.py`) **File**: `tests/unit/models/test_mappers.py` **Coverage**: API → Game model transformation ```python import pytest from app.models.api_models import SbaPlayerApi, PdPlayerApi from app.models.player_models import PlayerMapper class TestPlayerMapper: """Test player model mapping""" def test_extract_positions_multiple(self): """Test extracting multiple positions""" positions = PlayerMapper.extract_positions( "RF", "CF", "DH", None, None, None, None, None ) assert positions == ["RF", "CF", "DH"] def test_extract_positions_single(self): """Test extracting single position""" positions = PlayerMapper.extract_positions( "P", None, None, None, None, None, None, None ) assert positions == ["P"] def test_extract_positions_all_none(self): """Test with all None (edge case)""" positions = PlayerMapper.extract_positions( None, None, None, None, None, None, None, None ) assert positions == [] def test_map_sba_player(self, sba_player_api): """Test SBA player mapping""" game_player = PlayerMapper.map_sba_player(sba_player_api) assert game_player.player_id == sba_player_api.id assert game_player.name == sba_player_api.name assert game_player.team_name == sba_player_api.team.lname assert game_player.team_abbrev == sba_player_api.team.abbrev assert len(game_player.positions) > 0 def test_map_pd_player_position_player( self, pd_player_api, pd_batting_ratings_api ): """Test PD position player mapping""" game_player = PlayerMapper.map_pd_player( pd_player_api, pd_batting_ratings_api, None # No pitching ratings ) assert game_player.player_id == pd_player_api.player_id assert game_player.name == pd_player_api.p_name assert not game_player.is_pitcher assert game_player.batting_vs_lhp is not None assert game_player.batting_vs_rhp is not None assert game_player.pitching_vs_lhb is None def test_flatten_batting_rating_combines_doubles( self, pd_batting_rating_api ): """Test that double outcomes are combined""" # Assume API has: double_three=1.0, double_two=2.0, double_pull=3.0 game_rating = PlayerMapper._flatten_batting_rating(pd_batting_rating_api) # Should be combined to single "double" field assert game_rating.double == 6.0 # 1.0 + 2.0 + 3.0 ``` --- ## Integration Tests ### 1. API Client (`test_api_client.py`) **File**: `tests/integration/data/test_api_client.py` **Coverage**: HTTP calls with mocked responses ```python import pytest from unittest.mock import AsyncMock, patch from app.data.api_client import LeagueApiClient @pytest.mark.integration @pytest.mark.asyncio class TestLeagueApiClient: """Test API client with mocked HTTP responses""" async def test_fetch_sba_player_success(self, mock_httpx_response, sba_player_json): """Test successful SBA player fetch""" with patch('httpx.AsyncClient') as mock_client: mock_client.return_value.get = AsyncMock( return_value=mock_httpx_response(sba_player_json) ) async with LeagueApiClient("sba") as client: player = await client.get_player(12288) assert player.name == "Ronald Acuna Jr" async def test_fetch_pd_player_with_ratings( self, mock_httpx_response, pd_player_json, pd_batting_ratings_json ): """Test fetching PD player with ratings""" with patch('httpx.AsyncClient') as mock_client: # Mock responses for 3 API calls mock_client.return_value.get = AsyncMock( side_effect=[ mock_httpx_response(pd_player_json), mock_httpx_response(pd_batting_ratings_json), httpx.HTTPStatusError("Not a pitcher") ] ) async with LeagueApiClient("pd") as client: player = await client.get_player(10633) batting = await client.get_batting_ratings(10633) with pytest.raises(httpx.HTTPStatusError): await client.get_pitching_ratings(10633) async def test_api_client_context_manager_closes(self): """Test that context manager closes client""" client = LeagueApiClient("sba") assert client.client is not None async with client: pass # Use context manager # Client should be closed (aclose called) # Verify by checking if subsequent calls fail ``` --- ### 2. Full Pipeline (`test_player_pipeline.py`) **File**: `tests/integration/test_player_pipeline.py` **Coverage**: API → Mapper → Game Model ```python import pytest from app.models.player_models import PlayerFactory @pytest.mark.integration @pytest.mark.asyncio class TestPlayerPipeline: """Test complete pipeline from API to game model""" async def test_sba_player_full_pipeline(self, mock_sba_api): """Test fetching and mapping SBA player""" player = await PlayerFactory.create_player("sba", 12288) # Verify game model assert player.player_id == 12288 assert player.name == "Ronald Acuna Jr" assert "RF" in player.positions assert player.can_play_position("RF") async def test_pd_position_player_full_pipeline(self, mock_pd_api): """Test fetching and mapping PD position player""" player = await PlayerFactory.create_player("pd", 10633) # Verify game model assert player.player_id == 10633 assert player.name == "Chuck Knoblauch" assert not player.is_pitcher # Verify ratings attached assert player.batting_vs_lhp is not None assert player.batting_vs_rhp is not None assert player.pitching_vs_lhb is None async def test_pd_pitcher_full_pipeline(self, mock_pd_api): """Test fetching and mapping PD pitcher""" player = await PlayerFactory.create_player("pd", 11223) # Verify game model assert player.player_id == 11223 assert player.is_pitcher # Verify both batting and pitching ratings assert player.batting_vs_lhp is not None assert player.pitching_vs_lhb is not None assert player.pitching_vs_rhb is not None ``` --- ## Test Fixtures ### Shared Fixtures (`conftest.py`) ```python import pytest import json from pathlib import Path @pytest.fixture def sba_player_json(): """Load SBA player JSON from file""" path = Path(__file__).parent / "fixtures" / "sba_player.json" with open(path) as f: return json.load(f) @pytest.fixture def pd_player_json(): """Load PD player JSON from file""" path = Path(__file__).parent / "fixtures" / "pd_player.json" with open(path) as f: return json.load(f) @pytest.fixture def pd_batting_ratings_json(): """Load PD batting ratings JSON from file""" path = Path(__file__).parent / "fixtures" / "pd_batting_ratings.json" with open(path) as f: return json.load(f) @pytest.fixture def pd_pitching_ratings_json(): """Load PD pitching ratings JSON from file""" path = Path(__file__).parent / "fixtures" / "pd_pitching_ratings.json" with open(path) as f: return json.load(f) ``` ### Fixture JSON Files Store real API responses in `tests/fixtures/`: - `sba_player.json` - From user's example - `pd_player.json` - From user's example - `pd_batting_ratings.json` - From user's example - `pd_pitching_ratings.json` - From user's example --- ## Coverage Goals | Module | Target | Priority | |--------|--------|----------| | `api_models.py` | 95%+ | High | | `player_models.py` | 95%+ | High | | `api_client.py` | 85%+ | Medium | | Mappers | 95%+ | High | | Integration | 80%+ | Medium | --- ## Running Tests ```bash # All tests pytest tests/ -v # Only unit tests pytest tests/unit/ -v # Only integration tests pytest tests/integration/ -v -m integration # With coverage pytest tests/ --cov=app.models --cov=app.data --cov-report=html # Specific file pytest tests/unit/models/test_api_models.py -v ``` --- ## Test Markers ```python # pytest.ini [pytest] markers = integration: marks tests as integration tests (slow, may require network) unit: marks tests as unit tests (fast, no external dependencies) ``` Usage: ```bash # Run only unit tests pytest -m unit # Run only integration tests pytest -m integration # Skip integration tests pytest -m "not integration" ``` --- **Status**: Week 6 testing strategy complete. Ready for implementation!