Split player model architecture into dedicated documentation files for clarity and maintainability. Added Phase 1 status tracking and comprehensive player model specs covering API models, game models, mappers, and testing strategy. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
17 KiB
17 KiB
Testing Strategy for Player Models
Purpose: Comprehensive test plan for Week 6 player model implementation
Overview
Testing will be organized into three layers:
- Unit Tests - Individual components in isolation
- Integration Tests - Components working together
- 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
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
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
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
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
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)
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 examplepd_player.json- From user's examplepd_batting_ratings.json- From user's examplepd_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
# 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
# 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:
# 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!