# Test Factories This directory contains factory classes for generating unique, valid test data. Factories are essential for maintaining test isolation and preventing data conflicts between tests. ## 🚨 CRITICAL: Test Isolation Requirements **ALL tests must use these factories instead of manual model creation** to ensure: - ✅ Unique IDs prevent primary key conflicts - ✅ Consistent test data structure - ✅ Isolated test execution - ✅ Deterministic test results ## Factory Pattern Overview Each model has a corresponding factory that follows this pattern: ```python class ModelFactory: @staticmethod def build(**kwargs): """Build model instance without saving to database.""" defaults = { 'id': generate_unique_id(), 'field1': 'default_value', 'field2': generate_unique_name('Prefix') } defaults.update(kwargs) return Model(**defaults) @staticmethod def create(session, **kwargs): """Create and save model instance to database.""" instance = ModelFactory.build(**kwargs) session.add(instance) session.commit() session.refresh(instance) return instance ``` ## Available Factories ### TeamFactory (`team_factory.py`) **Purpose**: Generate Team instances with unique IDs and team data. **Basic Usage:** ```python from tests.factories.team_factory import TeamFactory # Build without saving team = TeamFactory.build(abbrev="LAD", lname="Los Angeles Dodgers") # Create and save to database team = TeamFactory.create(db_session, abbrev="LAD", wallet=50000) ``` **Specialized Methods:** ```python # Create AI team ai_team = TeamFactory.build_ai_team(abbrev="AI1") # Create human team human_team = TeamFactory.build_human_team(abbrev="HUM1") # Create multiple teams with unique IDs teams = TeamFactory.build_multiple(3, season=9) ``` **Default Values:** - `id`: Unique generated ID - `abbrev`: "TST" - `lname`: "Test Team" - `wallet`: 25000 - `is_ai`: False - All other required fields have sensible defaults ### CardsetFactory (`cardset_factory.py`) **Purpose**: Generate Cardset instances for testing card sets and game configurations. **Basic Usage:** ```python from tests.factories.cardset_factory import CardsetFactory # Build without saving cardset = CardsetFactory.build(name="2024 Season", ranked_legal=True) # Create and save to database cardset = CardsetFactory.create(db_session, name="Test Set") ``` **Specialized Methods:** ```python # Create ranked legal cardset ranked_set = CardsetFactory.create_ranked(db_session, name="Ranked Set") # Create multiple cardsets cardsets = CardsetFactory.create_batch(db_session, 3, ranked_legal=True) ``` **Default Values:** - `id`: Unique generated ID - `name`: "Test Cardset [unique]" - `ranked_legal`: False ### ManagerAiFactory (`manager_ai_factory.py`) **Purpose**: Generate ManagerAi instances for testing AI decision-making. **Basic Usage:** ```python from tests.factories.manager_ai_factory import ManagerAiFactory # Build AI with default settings ai = ManagerAiFactory.build_balanced() # Create aggressive AI ai = ManagerAiFactory.create_aggressive(db_session) ``` **Specialized Methods:** ```python # Predefined AI types balanced_ai = ManagerAiFactory.build_balanced() aggressive_ai = ManagerAiFactory.build_aggressive() conservative_ai = ManagerAiFactory.build_conservative() # Custom AI settings custom_ai = ManagerAiFactory.build(steal=10, running=8, hold=3) ``` ## Factory Usage Patterns ### ✅ Correct Usage Patterns **Basic Model Creation:** ```python def test_team_creation(db_session): team = TeamFactory.create(db_session, abbrev="BOS") assert team.abbrev == "BOS" assert team.id is not None ``` **Custom Field Values:** ```python def test_ai_team_behavior(db_session): ai_team = TeamFactory.create( db_session, is_ai=True, abbrev="AI1", wallet=100000 ) assert ai_team.is_ai is True ``` **Multiple Related Objects:** ```python def test_game_creation(db_session): home_team = TeamFactory.create(db_session, abbrev="HOME") away_team = TeamFactory.create(db_session, abbrev="AWAY") cardset = CardsetFactory.create(db_session, ranked_legal=True) # Test game creation with related objects # Game logic here... ``` **Batch Creation:** ```python def test_multiple_teams(db_session): teams = TeamFactory.build_multiple(5) for team in teams: db_session.add(team) db_session.commit() # All teams have unique IDs ids = [team.id for team in teams] assert len(set(ids)) == 5 ``` ### ❌ Anti-Patterns to Avoid **Manual Model Creation:** ```python # DON'T DO THIS - hardcoded IDs cause conflicts def test_bad_pattern(db_session): team = Team( id=1, # ❌ Hardcoded ID abbrev="TST", lname="Test Team", # ... many required fields ) ``` **Shared Mutable State:** ```python # DON'T DO THIS - shared state between tests SHARED_TEAM = Team(id=999, abbrev="SHARED") def test_bad_shared_state(db_session): db_session.add(SHARED_TEAM) # ❌ Modifies shared state ``` **Non-unique Values:** ```python # DON'T DO THIS - non-unique values cause conflicts def test_bad_non_unique(db_session): team1 = Team(id=1, abbrev="SAME") # ❌ Same ID team2 = Team(id=1, abbrev="SAME") # ❌ Same ID ``` ## Creating New Factories When adding new models, create corresponding factories following this template: ### 1. Create Factory File Create `tests/factories/model_name_factory.py`: ```python """ ModelName factory for generating test data. Provides methods to create unique, valid ModelName instances for testing without conflicts between test runs. """ from app.models.model_name import ModelName from tests.conftest import generate_unique_id, generate_unique_name class ModelNameFactory: """Factory for creating ModelName test instances.""" @staticmethod def build(**kwargs): """ Build a ModelName instance without saving to database. Args: **kwargs: Override default field values Returns: ModelName: Configured model instance Example: model = ModelNameFactory.build(field="custom_value") """ defaults = { 'id': generate_unique_id(), 'name': generate_unique_name('ModelName'), # Add all required fields with sensible defaults } # Override defaults with provided kwargs defaults.update(kwargs) return ModelName(**defaults) @staticmethod def create(session, **kwargs): """ Create and save a ModelName instance to the database. Args: session: Database session **kwargs: Override default field values Returns: ModelName: Saved model instance Example: model = ModelNameFactory.create(session, field="custom_value") """ instance = ModelNameFactory.build(**kwargs) session.add(instance) session.commit() session.refresh(instance) return instance # Add specialized factory methods as needed @staticmethod def build_special_type(**kwargs): """Build specialized variant of model.""" special_defaults = { 'special_field': 'special_value' } special_defaults.update(kwargs) return ModelNameFactory.build(**special_defaults) ``` ### 2. Update Factory __init__.py Add your factory to `tests/factories/__init__.py`: ```python from .model_name_factory import ModelNameFactory __all__ = [ # ... existing factories "ModelNameFactory", ] ``` ### 3. Add Factory Tests Create tests for your factory in `tests/unit/factories/test_model_name_factory.py`: ```python def test_build_creates_valid_instance(): model = ModelNameFactory.build() assert model.id is not None assert model.name is not None def test_create_saves_to_database(db_session): model = ModelNameFactory.create(db_session) retrieved = db_session.get(ModelName, model.id) assert retrieved is not None def test_unique_ids_generated(): models = [ModelNameFactory.build() for _ in range(5)] ids = [model.id for model in models] assert len(set(ids)) == 5 # All unique ``` ## Helper Functions ### `generate_unique_id()` Generates unique integer IDs using UUID hex conversion: ```python id = generate_unique_id() # Returns: 3847291847 ``` ### `generate_unique_name(prefix="Test")` Generates unique names with UUID suffix: ```python name = generate_unique_name("Team") # Returns: "Team a3b4c5d6" ``` ## Best Practices 1. **Always use factories** for test data creation 2. **Never hardcode IDs** - use `generate_unique_id()` 3. **Provide sensible defaults** for all required fields 4. **Override only what you need** in tests 5. **Use specialized methods** for common patterns 6. **Test your factories** to ensure they work correctly 7. **Keep factories simple** - complex logic belongs in services ## Integration with Tests **Required imports for new test files:** ```python from tests.factories.team_factory import TeamFactory from tests.factories.cardset_factory import CardsetFactory # Import other factories as needed ``` **Required fixture usage:** ```python def test_something(db_session): # Use db_session from conftest.py team = TeamFactory.create(db_session, abbrev="TEST") # Test logic here ``` This pattern ensures consistent, isolated, and reliable tests across the entire project.