Spaces:
Sleeping
Sleeping
test: add pytest suite for API endpoints
Browse files- Add tests for /health, /predict (valid, invalid data, missing auth)
- Add conftest.py with mocked pipeline fixture and test payload
- Add pytest.ini with pythonpath config
- Add pytest and pytest-cov dependencies to requirements.txt
- pytest.ini +2 -0
- requirements.txt +6 -0
- tests/conftest.py +36 -0
- tests/test_api.py +40 -0
pytest.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[pytest]
|
| 2 |
+
pythonpath = .
|
requirements.txt
CHANGED
|
@@ -3,6 +3,7 @@ annotated-types==0.7.0
|
|
| 3 |
anyio==4.12.1
|
| 4 |
certifi==2026.2.25
|
| 5 |
click==8.3.1
|
|
|
|
| 6 |
dnspython==2.8.0
|
| 7 |
email-validator==2.3.0
|
| 8 |
fastapi==0.133.1
|
|
@@ -14,18 +15,23 @@ httpcore==1.0.9
|
|
| 14 |
httptools==0.7.1
|
| 15 |
httpx==0.28.1
|
| 16 |
idna==3.11
|
|
|
|
| 17 |
Jinja2==3.1.6
|
| 18 |
joblib==1.5.3
|
| 19 |
markdown-it-py==4.0.0
|
| 20 |
MarkupSafe==3.0.3
|
| 21 |
mdurl==0.1.2
|
| 22 |
numpy==2.4.2
|
|
|
|
| 23 |
pandas==3.0.1
|
|
|
|
| 24 |
pydantic==2.12.5
|
| 25 |
pydantic-extra-types==2.11.0
|
| 26 |
pydantic-settings==2.13.1
|
| 27 |
pydantic_core==2.41.5
|
| 28 |
Pygments==2.19.2
|
|
|
|
|
|
|
| 29 |
python-dateutil==2.9.0.post0
|
| 30 |
python-dotenv==1.2.1
|
| 31 |
python-multipart==0.0.22
|
|
|
|
| 3 |
anyio==4.12.1
|
| 4 |
certifi==2026.2.25
|
| 5 |
click==8.3.1
|
| 6 |
+
coverage==7.13.4
|
| 7 |
dnspython==2.8.0
|
| 8 |
email-validator==2.3.0
|
| 9 |
fastapi==0.133.1
|
|
|
|
| 15 |
httptools==0.7.1
|
| 16 |
httpx==0.28.1
|
| 17 |
idna==3.11
|
| 18 |
+
iniconfig==2.3.0
|
| 19 |
Jinja2==3.1.6
|
| 20 |
joblib==1.5.3
|
| 21 |
markdown-it-py==4.0.0
|
| 22 |
MarkupSafe==3.0.3
|
| 23 |
mdurl==0.1.2
|
| 24 |
numpy==2.4.2
|
| 25 |
+
packaging==26.0
|
| 26 |
pandas==3.0.1
|
| 27 |
+
pluggy==1.6.0
|
| 28 |
pydantic==2.12.5
|
| 29 |
pydantic-extra-types==2.11.0
|
| 30 |
pydantic-settings==2.13.1
|
| 31 |
pydantic_core==2.41.5
|
| 32 |
Pygments==2.19.2
|
| 33 |
+
pytest==9.0.2
|
| 34 |
+
pytest-cov==7.0.0
|
| 35 |
python-dateutil==2.9.0.post0
|
| 36 |
python-dotenv==1.2.1
|
| 37 |
python-multipart==0.0.22
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from unittest.mock import MagicMock, patch
|
| 3 |
+
from fastapi.testclient import TestClient
|
| 4 |
+
from app.main import app
|
| 5 |
+
|
| 6 |
+
TEST_API_KEY = "test-secret-key"
|
| 7 |
+
|
| 8 |
+
VALID_PAYLOAD = {
|
| 9 |
+
"age": 35, "genre": 1, "statut_marital": "Marié(e)",
|
| 10 |
+
"poste": "Consultant", "domaine_etude": "Infra & Cloud",
|
| 11 |
+
"niveau_education": 3, "nombre_experiences_precedentes": 2,
|
| 12 |
+
"annee_experience_totale": 10, "annees_dans_l_entreprise": 5,
|
| 13 |
+
"annees_dans_le_poste_actuel": 2, "annees_sous_responsable_actuel": 3,
|
| 14 |
+
"annees_depuis_la_derniere_promotion": 1,
|
| 15 |
+
"note_evaluation_actuelle": 3, "note_evaluation_precedente": 3,
|
| 16 |
+
"augmentation_salaire_precedente": 15, "nb_formations_suivies": 2,
|
| 17 |
+
"nombre_participation_pee": 1,
|
| 18 |
+
"satisfaction_employee_environnement": 3,
|
| 19 |
+
"satisfaction_employee_nature_travail": 4,
|
| 20 |
+
"satisfaction_employee_equipe": 3,
|
| 21 |
+
"satisfaction_employee_equilibre_pro_perso": 2,
|
| 22 |
+
"heure_supplementaires": 0, "frequence_deplacement": 1,
|
| 23 |
+
"distance_domicile_travail": 10, "revenu_mensuel": 5000,
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
@pytest.fixture(scope="module")
|
| 27 |
+
def client():
|
| 28 |
+
mock_pipeline = MagicMock()
|
| 29 |
+
# Simule predict_proba → [[proba_reste, proba_quitte]]
|
| 30 |
+
# 0.3 < seuil 0.40 → prédiction "Reste"
|
| 31 |
+
mock_pipeline.predict_proba.return_value = [[0.70, 0.30]]
|
| 32 |
+
|
| 33 |
+
with patch("app.main.load_pipeline", return_value=(mock_pipeline, 0.40)), \
|
| 34 |
+
patch.dict("os.environ", {"API_KEY": TEST_API_KEY}):
|
| 35 |
+
with TestClient(app) as c:
|
| 36 |
+
yield c
|
tests/test_api.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from tests.conftest import VALID_PAYLOAD, TEST_API_KEY
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
# Tester GET /health
|
| 5 |
+
def test_health_check(client):
|
| 6 |
+
response = client.get("/health")
|
| 7 |
+
assert response.status_code == 200
|
| 8 |
+
assert response.json()["status"] == "ok"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# Tester POST /predict avec données valides
|
| 12 |
+
def test_predict_valid(client):
|
| 13 |
+
response = client.post(
|
| 14 |
+
"/predict",
|
| 15 |
+
json=VALID_PAYLOAD,
|
| 16 |
+
headers={"X-API-Key": TEST_API_KEY},
|
| 17 |
+
)
|
| 18 |
+
assert response.status_code == 200
|
| 19 |
+
body = response.json()
|
| 20 |
+
assert "label" in body
|
| 21 |
+
assert body["label"] in ("Reste", "Quitte")
|
| 22 |
+
assert "probabilite" in body
|
| 23 |
+
assert 0.0 <= body["probabilite"] <= 1.0
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# Tester POST /predict avec données invalides
|
| 27 |
+
def test_predict_invalid_data(client):
|
| 28 |
+
invalid_payload = {"age": 999, "genre": 99} # hors bornes + champs manquants
|
| 29 |
+
response = client.post(
|
| 30 |
+
"/predict",
|
| 31 |
+
json=invalid_payload,
|
| 32 |
+
headers={"X-API-Key": TEST_API_KEY},
|
| 33 |
+
)
|
| 34 |
+
assert response.status_code == 422 # Pydantic validation error
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# Tester POST /predict sans API Key → 401
|
| 38 |
+
def test_predict_no_api_key(client):
|
| 39 |
+
response = client.post("/predict", json=VALID_PAYLOAD)
|
| 40 |
+
assert response.status_code == 401
|