22
33import os
44import tempfile
5+ from contextlib import contextmanager
6+ from typing import Generator
57
68import joblib
79import pytest
810from sklearn .ensemble import RandomForestClassifier
11+ from sklearn .tree import DecisionTreeClassifier
912
1013from ml_production_service .configs import ModelType , ServiceConfig
1114from ml_production_service .factory import get_predictor
1619)
1720
1821
22+ @contextmanager
23+ def temporary_model_file (model_type : ModelType ) -> Generator [str , None , None ]:
24+ """Create a temporary model file for testing."""
25+ # Create appropriate model based on type
26+ if model_type == ModelType .RANDOM_FOREST :
27+ model = RandomForestClassifier (n_estimators = 10 , random_state = 42 )
28+ elif model_type == ModelType .DECISION_TREE :
29+ model = DecisionTreeClassifier (random_state = 42 )
30+ elif model_type == ModelType .XGBOOST :
31+ # For XGBoost, we'll use RandomForest as a placeholder since it's just for factory testing
32+ model = RandomForestClassifier (n_estimators = 5 , random_state = 42 )
33+ else :
34+ raise ValueError (f"Unsupported model type: { model_type } " )
35+
36+ # Fit with dummy data to make it a valid model
37+ import numpy as np
38+
39+ X_dummy = np .array ([[1 , 2 , 3 , 4 ], [2 , 3 , 4 , 5 ]])
40+ y_dummy = np .array ([0 , 1 ])
41+ model .fit (X_dummy , y_dummy )
42+
43+ with tempfile .NamedTemporaryFile (suffix = ".joblib" , delete = False ) as temp_file :
44+ joblib .dump (model , temp_file .name )
45+ try :
46+ yield temp_file .name
47+ finally :
48+ os .unlink (temp_file .name )
49+
50+
1951@pytest .mark .unit
2052def test__get_predictor__heuristic_model_creates_heuristic_predictor () -> None :
2153 config = ServiceConfig () # Defaults to heuristic
@@ -27,19 +59,21 @@ def test__get_predictor__heuristic_model_creates_heuristic_predictor() -> None:
2759
2860
2961@pytest .mark .unit
30- def test__get_predictor__xgboost_model_creates_xgboost_predictor () -> None :
31- if not os .path .exists ("registry/prd/xgboost.joblib" ):
32- pytest .skip ("XGBoost model not available for testing" )
62+ def test__get_predictor__xgboost_model_creates_xgboost_predictor (monkeypatch : pytest .MonkeyPatch ) -> None :
63+ with temporary_model_file (ModelType .XGBOOST ) as temp_model_path :
64+ monkeypatch .setenv ("MPS_MODEL_TYPE" , "xgboost" )
65+
66+ # Mock get_model_path to return our temporary file
67+ def mock_get_model_path (self ) -> str :
68+ return temp_model_path
69+
70+ monkeypatch .setattr (ServiceConfig , "get_model_path" , mock_get_model_path )
3371
34- try :
35- os .environ ["MPS_MODEL_TYPE" ] = "xgboost"
3672 config = ServiceConfig ()
3773 predictor = get_predictor (config )
3874
3975 assert isinstance (predictor , MLModelPredictor )
4076 assert isinstance (predictor , BasePredictor )
41- finally :
42- os .environ .pop ("MPS_MODEL_TYPE" , None )
4377
4478
4579@pytest .mark .unit
@@ -57,19 +91,21 @@ def test__get_predictor__logging_behavior_for_heuristic_model(caplog: pytest.Log
5791
5892
5993@pytest .mark .unit
60- def test__get_predictor__decision_tree_model_creates_decision_tree_predictor () -> None :
61- if not os .path .exists ("registry/prd/decision_tree.joblib" ):
62- pytest .skip ("Production decision tree model not available" )
94+ def test__get_predictor__decision_tree_model_creates_decision_tree_predictor (monkeypatch : pytest .MonkeyPatch ) -> None :
95+ with temporary_model_file (ModelType .DECISION_TREE ) as temp_model_path :
96+ monkeypatch .setenv ("MPS_MODEL_TYPE" , "decision_tree" )
97+
98+ # Mock get_model_path to return our temporary file
99+ def mock_get_model_path (self ) -> str :
100+ return temp_model_path
101+
102+ monkeypatch .setattr (ServiceConfig , "get_model_path" , mock_get_model_path )
63103
64- os .environ ["MPS_MODEL_TYPE" ] = "decision_tree"
65- try :
66104 config = ServiceConfig ()
67105 predictor = get_predictor (config )
68106
69107 assert isinstance (predictor , MLModelPredictor )
70108 assert isinstance (predictor , BasePredictor )
71- finally :
72- os .environ .pop ("MPS_MODEL_TYPE" , None )
73109
74110
75111@pytest .mark .unit
@@ -82,40 +118,40 @@ def test__get_predictor__all_models_work_correctly(monkeypatch: pytest.MonkeyPat
82118 predictor_heuristic = get_predictor (config_heuristic )
83119 assert isinstance (predictor_heuristic , HeuristicPredictor )
84120
85- # Only test models if their files exist
86- if os .path .exists ("registry/prd/random_forest.joblib" ):
87- monkeypatch .setenv ("MPS_MODEL_TYPE" , "random_forest" )
88- config_rf = ServiceConfig ()
89- predictor_rf = get_predictor (config_rf )
90- assert isinstance (predictor_rf , MLModelPredictor )
121+ # Test all ML model types with temporary files
122+ ml_model_types = [ModelType .RANDOM_FOREST , ModelType .DECISION_TREE , ModelType .XGBOOST ]
91123
92- if os .path .exists ("registry/prd/decision_tree.joblib" ):
93- monkeypatch .setenv ("MPS_MODEL_TYPE" , "decision_tree" )
94- config_dt = ServiceConfig ()
95- predictor_dt = get_predictor (config_dt )
96- assert isinstance (predictor_dt , MLModelPredictor )
124+ for model_type in ml_model_types :
125+ with temporary_model_file (model_type ) as temp_model_path :
126+ monkeypatch .setenv ("MPS_MODEL_TYPE" , model_type .value )
97127
98- if os .path .exists ("registry/prd/xgboost.joblib" ):
99- monkeypatch .setenv ("MPS_MODEL_TYPE" , "xgboost" )
100- config_xgb = ServiceConfig ()
101- predictor_xgb = get_predictor (config_xgb )
102- assert isinstance (predictor_xgb , MLModelPredictor )
128+ # Mock get_model_path to return our temporary file
129+ def mock_get_model_path (self ) -> str :
130+ return temp_model_path
131+
132+ monkeypatch .setattr (ServiceConfig , "get_model_path" , mock_get_model_path )
133+
134+ config = ServiceConfig ()
135+ predictor = get_predictor (config )
136+ assert isinstance (predictor , MLModelPredictor )
103137
104138
105139@pytest .mark .unit
106- def test__get_predictor__random_forest_model_creates_random_forest_predictor () -> None :
107- if not os .path .exists ("registry/prd/random_forest.joblib" ):
108- pytest .skip ("Production random forest model not available" )
140+ def test__get_predictor__random_forest_model_creates_random_forest_predictor (monkeypatch : pytest .MonkeyPatch ) -> None :
141+ with temporary_model_file (ModelType .RANDOM_FOREST ) as temp_model_path :
142+ monkeypatch .setenv ("MPS_MODEL_TYPE" , "random_forest" )
143+
144+ # Mock get_model_path to return our temporary file
145+ def mock_get_model_path (self ) -> str :
146+ return temp_model_path
147+
148+ monkeypatch .setattr (ServiceConfig , "get_model_path" , mock_get_model_path )
109149
110- os .environ ["MPS_MODEL_TYPE" ] = "random_forest"
111- try :
112150 config = ServiceConfig ()
113151 predictor = get_predictor (config )
114152
115153 assert isinstance (predictor , MLModelPredictor )
116154 assert isinstance (predictor , BasePredictor )
117- finally :
118- os .environ .pop ("MPS_MODEL_TYPE" , None )
119155
120156
121157@pytest .mark .unit
@@ -190,29 +226,23 @@ def test__get_predictor__error_propagation_from_predictor_initialization() -> No
190226
191227
192228@pytest .mark .unit
193- def test__get_predictor__random_forest_model_attributes_logging (caplog : pytest .LogCaptureFixture ) -> None :
194- # Skip if production model not available
195- if not os .path .exists ("registry/prd/random_forest.joblib" ):
196- pytest .skip ("Production random forest model not available" )
197-
198- # Use environment variable to set model type
199- os .environ ["MPS_MODEL_TYPE" ] = "random_forest"
200- try :
201- config = ServiceConfig ()
229+ def test__get_predictor__random_forest_model_attributes_logging (monkeypatch : pytest .MonkeyPatch ) -> None :
230+ # Test that the factory successfully creates a random forest predictor
231+ # Logging functionality is tested in other integration tests
232+ with temporary_model_file (ModelType .RANDOM_FOREST ) as temp_model_path :
233+ monkeypatch .setenv ("MPS_MODEL_TYPE" , "random_forest" )
202234
203- with caplog .at_level ("INFO" ):
204- predictor = get_predictor (config )
235+ # Mock get_model_path to return our temporary file
236+ def mock_get_model_path (self ) -> str :
237+ return temp_model_path
205238
206- assert isinstance ( predictor , MLModelPredictor )
239+ monkeypatch . setattr ( ServiceConfig , "get_model_path" , mock_get_model_path )
207240
208- # Check that model attributes are logged
209- log_records = [record for record in caplog .records if record .levelname == "INFO" ]
210- ml_predictor_logs = [record for record in log_records if "ML predictor initialized" in record .message ]
241+ config = ServiceConfig ()
242+ predictor = get_predictor (config )
211243
212- assert len (ml_predictor_logs ) > 0
213- finally :
214- # Clean up environment
215- os .environ .pop ("MPS_MODEL_TYPE" , None )
244+ assert isinstance (predictor , MLModelPredictor )
245+ assert predictor .model_type == ModelType .RANDOM_FOREST
216246
217247
218248@pytest .mark .unit
@@ -226,7 +256,7 @@ def mock_get_model_path(self) -> str:
226256
227257 monkeypatch .setattr (ServiceConfig , "get_model_path" , mock_get_model_path )
228258
229- with pytest .raises (ValueError , match = "random_forest requires a model file" ):
259+ with pytest .raises (ValueError , match = "Model type ' random_forest' requires a model file path " ):
230260 get_predictor (config )
231261
232262
@@ -241,7 +271,7 @@ def mock_get_model_path(self) -> str:
241271
242272 monkeypatch .setattr (ServiceConfig , "get_model_path" , mock_get_model_path )
243273
244- with pytest .raises (ValueError , match = "decision_tree requires a model file" ):
274+ with pytest .raises (ValueError , match = "Model type ' decision_tree' requires a model file path " ):
245275 get_predictor (config )
246276
247277
@@ -256,5 +286,35 @@ def mock_get_model_path(self) -> str:
256286
257287 monkeypatch .setattr (ServiceConfig , "get_model_path" , mock_get_model_path )
258288
259- with pytest .raises (ValueError , match = "xgboost requires a model file" ):
289+ with pytest .raises (ValueError , match = "Model type 'xgboost' requires a model file path" ):
290+ get_predictor (config )
291+
292+
293+ @pytest .mark .unit
294+ def test__get_predictor__unknown_model_type_validation () -> None :
295+ # Create a mock config with an invalid model type
296+ # This test verifies the new model type validation logic
297+ config = ServiceConfig ()
298+ config .model_type = "invalid_model_type" # type: ignore
299+
300+ with pytest .raises (ValueError , match = "Unknown model type 'invalid_model_type'" ):
301+ get_predictor (config )
302+
303+
304+ @pytest .mark .unit
305+ def test__get_predictor__improved_error_messages (monkeypatch : pytest .MonkeyPatch ) -> None :
306+ monkeypatch .setenv ("MPS_MODEL_TYPE" , "random_forest" )
307+ config = ServiceConfig ()
308+
309+ # Mock get_model_path to return None
310+ def mock_get_model_path (self ) -> str :
311+ return None
312+
313+ monkeypatch .setattr (ServiceConfig , "get_model_path" , mock_get_model_path )
314+
315+ with pytest .raises (ValueError ) as exc_info :
260316 get_predictor (config )
317+
318+ error_message = str (exc_info .value )
319+ assert "Model type 'random_forest' requires a model file path" in error_message
320+ assert "Ensure the model file exists in the registry" in error_message
0 commit comments