Fix generating pytest for training service 3
This commit is contained in:
311
services/training/tests/conftest.py
Normal file
311
services/training/tests/conftest.py
Normal file
@@ -0,0 +1,311 @@
|
||||
# services/training/tests/conftest.py
|
||||
"""
|
||||
Test configuration and fixtures for training service ML components
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from typing import Dict, List, Any, Generator
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
|
||||
# Configure test environment
|
||||
os.environ["MODEL_STORAGE_PATH"] = "/tmp/test_models"
|
||||
os.environ["TRAINING_DATABASE_URL"] = "sqlite+aiosqlite:///:memory:"
|
||||
|
||||
# Create test event loop
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for the test session."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
# ================================================================
|
||||
# PYTEST CONFIGURATION
|
||||
# ================================================================
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Configure pytest markers"""
|
||||
config.addinivalue_line("markers", "unit: Unit tests")
|
||||
config.addinivalue_line("markers", "integration: Integration tests")
|
||||
config.addinivalue_line("markers", "ml: Machine learning tests")
|
||||
config.addinivalue_line("markers", "slow: Slow-running tests")
|
||||
|
||||
# ================================================================
|
||||
# MOCK SETTINGS AND CONFIGURATION
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_settings():
|
||||
"""Mock settings for all tests"""
|
||||
with patch('app.core.config.settings') as mock_settings:
|
||||
mock_settings.MODEL_STORAGE_PATH = "/tmp/test_models"
|
||||
mock_settings.MIN_TRAINING_DATA_DAYS = 30
|
||||
mock_settings.PROPHET_SEASONALITY_MODE = "additive"
|
||||
mock_settings.PROPHET_CHANGEPOINT_PRIOR_SCALE = 0.05
|
||||
mock_settings.PROPHET_SEASONALITY_PRIOR_SCALE = 10.0
|
||||
mock_settings.PROPHET_HOLIDAYS_PRIOR_SCALE = 10.0
|
||||
mock_settings.ENABLE_SPANISH_HOLIDAYS = True
|
||||
mock_settings.ENABLE_MADRID_HOLIDAYS = True
|
||||
|
||||
# Ensure test model directory exists
|
||||
os.makedirs("/tmp/test_models", exist_ok=True)
|
||||
|
||||
yield mock_settings
|
||||
|
||||
# ================================================================
|
||||
# MOCK ML COMPONENTS
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def mock_prophet_manager():
|
||||
"""Mock BakeryProphetManager for testing"""
|
||||
mock_manager = AsyncMock()
|
||||
|
||||
# Mock train_bakery_model method
|
||||
mock_manager.train_bakery_model.return_value = {
|
||||
'model_id': f'test-model-{uuid.uuid4().hex[:8]}',
|
||||
'model_path': '/tmp/test_models/test_model.pkl',
|
||||
'type': 'prophet',
|
||||
'training_samples': 100,
|
||||
'features': ['temperature', 'humidity', 'day_of_week'],
|
||||
'training_metrics': {
|
||||
'mae': 5.2,
|
||||
'rmse': 7.8,
|
||||
'r2': 0.85
|
||||
},
|
||||
'created_at': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Mock validate_training_data method
|
||||
mock_manager._validate_training_data = AsyncMock()
|
||||
|
||||
# Mock generate_forecast method
|
||||
mock_manager.generate_forecast.return_value = pd.DataFrame({
|
||||
'ds': pd.date_range('2024-02-01', periods=7, freq='D'),
|
||||
'yhat': [50.0] * 7,
|
||||
'yhat_lower': [45.0] * 7,
|
||||
'yhat_upper': [55.0] * 7
|
||||
})
|
||||
|
||||
# Mock other methods
|
||||
mock_manager._get_spanish_holidays.return_value = pd.DataFrame({
|
||||
'holiday': ['new_year', 'christmas'],
|
||||
'ds': [datetime(2024, 1, 1), datetime(2024, 12, 25)]
|
||||
})
|
||||
|
||||
mock_manager._extract_regressor_columns.return_value = ['temperature', 'humidity']
|
||||
|
||||
return mock_manager
|
||||
|
||||
@pytest.fixture
|
||||
def mock_data_processor():
|
||||
"""Mock BakeryDataProcessor for testing"""
|
||||
mock_processor = AsyncMock()
|
||||
|
||||
# Mock prepare_training_data method
|
||||
mock_processor.prepare_training_data.return_value = pd.DataFrame({
|
||||
'ds': pd.date_range('2024-01-01', periods=35, freq='D'),
|
||||
'y': [45 + 5 * np.sin(i / 7) for i in range(35)],
|
||||
'temperature': [15.0] * 35,
|
||||
'humidity': [65.0] * 35,
|
||||
'day_of_week': [i % 7 for i in range(35)],
|
||||
'is_weekend': [1 if i % 7 >= 5 else 0 for i in range(35)],
|
||||
'month': [1] * 35,
|
||||
'is_holiday': [0] * 35
|
||||
})
|
||||
|
||||
# Mock prepare_prediction_features method
|
||||
mock_processor.prepare_prediction_features.return_value = pd.DataFrame({
|
||||
'ds': pd.date_range('2024-02-01', periods=7, freq='D'),
|
||||
'temperature': [18.0] * 7,
|
||||
'humidity': [65.0] * 7,
|
||||
'day_of_week': [i % 7 for i in range(7)],
|
||||
'is_weekend': [1 if i % 7 >= 5 else 0 for i in range(7)],
|
||||
'month': [2] * 7,
|
||||
'is_holiday': [0] * 7
|
||||
})
|
||||
|
||||
# Mock private methods for testing
|
||||
mock_processor._add_temporal_features.return_value = pd.DataFrame({
|
||||
'date': pd.date_range('2024-01-01', periods=10, freq='D'),
|
||||
'day_of_week': [i % 7 for i in range(10)],
|
||||
'is_weekend': [1 if i % 7 >= 5 else 0 for i in range(10)],
|
||||
'month': [1] * 10,
|
||||
'season': ['winter'] * 10,
|
||||
'week_of_year': [1] * 10,
|
||||
'quarter': [1] * 10,
|
||||
'is_holiday': [0] * 10,
|
||||
'is_school_holiday': [0] * 10
|
||||
})
|
||||
|
||||
mock_processor._is_spanish_holiday.return_value = False
|
||||
|
||||
return mock_processor
|
||||
|
||||
# ================================================================
|
||||
# SAMPLE DATA FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def sample_sales_data():
|
||||
"""Generate sample sales data for testing"""
|
||||
dates = pd.date_range('2024-01-01', periods=35, freq='D')
|
||||
data = []
|
||||
for i, date in enumerate(dates):
|
||||
data.append({
|
||||
'date': date,
|
||||
'product_name': 'Pan Integral',
|
||||
'quantity': 40 + (5 * np.sin(i / 7)) + np.random.normal(0, 2)
|
||||
})
|
||||
return pd.DataFrame(data)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_weather_data():
|
||||
"""Generate sample weather data for testing"""
|
||||
dates = pd.date_range('2024-01-01', periods=60, freq='D')
|
||||
return pd.DataFrame({
|
||||
'date': dates,
|
||||
'temperature': [15 + 5 * np.sin(2 * np.pi * i / 365) + np.random.normal(0, 2) for i in range(60)],
|
||||
'precipitation': [max(0, np.random.exponential(1)) for _ in range(60)],
|
||||
'humidity': [60 + np.random.normal(0, 10) for _ in range(60)]
|
||||
})
|
||||
|
||||
@pytest.fixture
|
||||
def sample_traffic_data():
|
||||
"""Generate sample traffic data for testing"""
|
||||
dates = pd.date_range('2024-01-01', periods=60, freq='D')
|
||||
return pd.DataFrame({
|
||||
'date': dates,
|
||||
'traffic_volume': [100 + np.random.normal(0, 20) for _ in range(60)]
|
||||
})
|
||||
|
||||
@pytest.fixture
|
||||
def sample_prophet_data():
|
||||
"""Generate sample data in Prophet format for testing"""
|
||||
dates = pd.date_range('2024-01-01', periods=100, freq='D')
|
||||
return pd.DataFrame({
|
||||
'ds': dates,
|
||||
'y': [45 + 10 * np.sin(2 * np.pi * i / 7) + np.random.normal(0, 5) for i in range(100)],
|
||||
'temperature': [15 + 5 * np.sin(2 * np.pi * i / 365) for i in range(100)],
|
||||
'humidity': [60 + np.random.normal(0, 10) for _ in range(100)]
|
||||
})
|
||||
|
||||
@pytest.fixture
|
||||
def sample_sales_records():
|
||||
"""Generate sample sales records as list of dicts"""
|
||||
return [
|
||||
{"date": "2024-01-01", "product_name": "Pan Integral", "quantity": 45},
|
||||
{"date": "2024-01-02", "product_name": "Pan Integral", "quantity": 50},
|
||||
{"date": "2024-01-03", "product_name": "Pan Integral", "quantity": 48},
|
||||
{"date": "2024-01-04", "product_name": "Croissant", "quantity": 25},
|
||||
{"date": "2024-01-05", "product_name": "Croissant", "quantity": 30}
|
||||
]
|
||||
|
||||
# ================================================================
|
||||
# UTILITY FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def temp_model_dir():
|
||||
"""Create a temporary directory for model storage"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
yield temp_dir
|
||||
|
||||
@pytest.fixture
|
||||
def test_tenant_id():
|
||||
"""Generate a test tenant ID"""
|
||||
return f"test-tenant-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
@pytest.fixture
|
||||
def test_job_id():
|
||||
"""Generate a test job ID"""
|
||||
return f"test-job-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# ================================================================
|
||||
# MOCK EXTERNAL DEPENDENCIES (Simplified)
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def mock_prophet_model():
|
||||
"""Create a mock Prophet model for testing"""
|
||||
mock_model = Mock()
|
||||
mock_model.fit.return_value = None
|
||||
mock_model.predict.return_value = pd.DataFrame({
|
||||
'ds': pd.date_range('2024-02-01', periods=7, freq='D'),
|
||||
'yhat': [50.0] * 7,
|
||||
'yhat_lower': [45.0] * 7,
|
||||
'yhat_upper': [55.0] * 7
|
||||
})
|
||||
mock_model.add_regressor.return_value = None
|
||||
return mock_model
|
||||
|
||||
# ================================================================
|
||||
# DATABASE MOCKS
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session():
|
||||
"""Mock database session for testing"""
|
||||
mock_session = AsyncMock()
|
||||
mock_session.commit = AsyncMock()
|
||||
mock_session.rollback = AsyncMock()
|
||||
mock_session.close = AsyncMock()
|
||||
mock_session.add = Mock()
|
||||
mock_session.execute = AsyncMock()
|
||||
mock_session.scalar = AsyncMock()
|
||||
mock_session.scalars = AsyncMock()
|
||||
return mock_session
|
||||
|
||||
# ================================================================
|
||||
# PERFORMANCE TESTING
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def performance_tracker():
|
||||
"""Performance tracking utilities for tests"""
|
||||
|
||||
class PerformanceTracker:
|
||||
def __init__(self):
|
||||
self.start_time = None
|
||||
self.measurements = {}
|
||||
|
||||
def start(self, operation_name: str = "default"):
|
||||
self.start_time = datetime.now()
|
||||
self.operation_name = operation_name
|
||||
|
||||
def stop(self) -> float:
|
||||
if self.start_time:
|
||||
duration = (datetime.now() - self.start_time).total_seconds() * 1000
|
||||
self.measurements[self.operation_name] = duration
|
||||
return duration
|
||||
return 0.0
|
||||
|
||||
def assert_performance(self, max_duration_ms: float, operation_name: str = "default"):
|
||||
duration = self.measurements.get(operation_name, float('inf'))
|
||||
assert duration <= max_duration_ms, f"Operation {operation_name} took {duration:.0f}ms, expected <= {max_duration_ms}ms"
|
||||
|
||||
return PerformanceTracker()
|
||||
|
||||
# ================================================================
|
||||
# CLEANUP
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_after_test():
|
||||
"""Automatic cleanup after each test"""
|
||||
yield
|
||||
# Clean up any test model files
|
||||
test_model_path = "/tmp/test_models"
|
||||
if os.path.exists(test_model_path):
|
||||
for file in os.listdir(test_model_path):
|
||||
try:
|
||||
os.remove(os.path.join(test_model_path, file))
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
Reference in New Issue
Block a user