OC_P8 / tests /unit /test_predictor.py
KLEB38's picture
Upload folder using huggingface_hub
f63ae60 verified
Raw
History Blame Contribute Delete
4.45 kB
"""Unit tests for CreditScoringPredictor.
The predictor is duck-typed on a `predict_fn` callable, so these tests inject
fake functions directly rather than round-tripping through a real .onnx file.
The full load() path (ONNX session + model_info.json) is exercised by the
integration tests with a real model.
"""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pandas as pd
import pytest
from api.predictor import CreditScoringPredictor
def _fixed_predict_fn(proba: float):
"""Build a predict_fn that always returns the given positive-class proba."""
def fn(arr: np.ndarray) -> np.ndarray:
return np.array([[1 - proba, proba]])
return fn
class _FakeOnnxSession:
"""Stand-in for ort.InferenceSession used to test the load() contract.
Exposes the same get_inputs/get_outputs/run surface as a real session
without requiring a real .onnx file. Used by tests that exercise
CreditScoringPredictor.load — keeps the session-wiring detail
out of every test body.
"""
def get_inputs(self):
return [type("X", (), {"name": "input"})()]
def get_outputs(self):
return [
type("L", (), {"name": "label"})(),
type("P", (), {"name": "probabilities"})(),
]
def run(self, _outputs, _feed):
return [np.array([[0.6, 0.4]])]
@pytest.fixture
def patch_onnx_session(monkeypatch):
"""Replace ort.InferenceSession with the fake so load() can be unit-tested."""
monkeypatch.setattr(
"api.predictor.ort.InferenceSession", lambda *_a, **_kw: _FakeOnnxSession()
)
def _build(proba: float, threshold: float) -> CreditScoringPredictor:
return CreditScoringPredictor(
predict_fn=_fixed_predict_fn(proba),
threshold=threshold,
model_version="test",
)
def _make_info(path: Path, threshold: float | None = 0.33) -> Path:
metrics = {"best_threshold_mean": threshold} if threshold is not None else {}
path.write_text(json.dumps({"version": "test-1", "metrics": metrics}))
return path
def test_threshold_property_round_trip():
assert _build(proba=0.1, threshold=0.42).threshold == 0.42
def test_model_version_property_round_trip():
pred = CreditScoringPredictor(
predict_fn=_fixed_predict_fn(0.0),
threshold=0.5,
model_version="abc-123",
)
assert pred.model_version == "abc-123"
@pytest.mark.parametrize(
"proba, expected",
[
(0.10, "GRANTED"), # below threshold
(0.50, "REFUSED"), # above threshold
(0.33, "REFUSED"), # boundary: proba >= threshold → REFUSED
],
)
def test_decision_logic(proba, expected):
p, decision = _build(proba=proba, threshold=0.33).predict(pd.DataFrame([{"x": 1.0}]))
assert p == pytest.approx(proba)
assert decision == expected
def test_proba_is_continuous_not_label():
"""Regression: predict() must surface the proba, not a class label."""
proba, _ = _build(proba=0.27, threshold=0.5).predict(pd.DataFrame([{"x": 1.0}]))
assert proba == pytest.approx(0.27)
def test_features_are_converted_to_float32():
"""The predict_fn must receive a float32 numpy array (ONNX requirement)."""
captured: dict[str, np.ndarray] = {}
def capturing_fn(arr: np.ndarray) -> np.ndarray:
captured["arr"] = arr
return np.array([[0.7, 0.3]])
pred = CreditScoringPredictor(
predict_fn=capturing_fn, threshold=0.5, model_version="v1"
)
pred.predict(pd.DataFrame([{"a": 1.0, "b": 2.0}]))
assert captured["arr"].dtype == np.float32
assert captured["arr"].shape == (1, 2)
def test_load_reads_threshold_from_model_info(tmp_path, patch_onnx_session):
"""load() must resolve threshold from model_info.json without touching ONNX."""
info_path = _make_info(tmp_path / "info.json", threshold=0.42)
pred = CreditScoringPredictor.load(
model_path=tmp_path / "dummy.onnx",
model_info_path=info_path,
default_threshold=0.5,
)
assert pred.threshold == 0.42
assert pred.model_version == "test-1"
def test_load_falls_back_to_default_threshold(tmp_path, patch_onnx_session):
info_path = _make_info(tmp_path / "info.json", threshold=None)
pred = CreditScoringPredictor.load(
model_path=tmp_path / "dummy.onnx",
model_info_path=info_path,
default_threshold=0.7,
)
assert pred.threshold == 0.7