Skip to content

Commit 591ae35

Browse files
creted pytests
1 parent 9b5c80e commit 591ae35

3 files changed

Lines changed: 171 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
name: Run backend tests
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: [3.11]
16+
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
26+
- name: Cache pip
27+
uses: actions/cache@v4
28+
with:
29+
path: ~/.cache/pip
30+
key: ${{ runner.os }}-pip-${{ hashFiles('**/backend/requirements.txt') }}
31+
restore-keys: |
32+
${{ runner.os }}-pip-
33+
34+
- name: Install dependencies
35+
run: |
36+
python -m pip install --upgrade pip
37+
pip install -r backend/requirements.txt
38+
39+
- name: Run pytest
40+
run: |
41+
mkdir -p reports
42+
pytest backend/tests -q --junitxml=reports/junit.xml
43+
44+
- name: Upload test report
45+
if: always()
46+
uses: actions/upload-artifact@v4
47+
with:
48+
name: pytest-report
49+
path: reports/junit.xml

backend/docker/Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Multi-stage build for MzeeChakula Backend API
22

3-
43
# Builder stage
54
FROM python:3.12-slim AS builder
65

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import os
2+
import pickle
3+
import json
4+
import numpy as np
5+
import tempfile
6+
from pathlib import Path
7+
8+
import pytest
9+
from fastapi.testclient import TestClient
10+
11+
12+
def make_dummy_model(pred_value=123.45, feature_names=None):
13+
class DummyModel:
14+
def __init__(self, v, cols=None):
15+
self._v = v
16+
if cols is not None:
17+
# mimic scikit-learn attribute
18+
self.feature_names_in_ = cols
19+
20+
def predict(self, X):
21+
# return array-like
22+
return [self._v] * len(X)
23+
24+
return DummyModel(pred_value, feature_names)
25+
26+
27+
@pytest.fixture
28+
def tmp_models_dir(tmp_path, monkeypatch):
29+
d = tmp_path / "models"
30+
d.mkdir()
31+
32+
# Create feature names pickle
33+
feature_names = [
34+
"Energy_kcal_per_serving",
35+
"Protein_g_per_serving",
36+
"Fat_g_per_serving",
37+
"Carbohydrates_g_per_serving",
38+
"Fiber_g_per_serving",
39+
"Calcium_mg_per_serving",
40+
"Iron_mg_per_serving",
41+
"Zinc_mg_per_serving",
42+
"VitaminA_ug_per_serving",
43+
"VitaminC_mg_per_serving",
44+
"Potassium_mg_per_serving",
45+
"Magnesium_mg_per_serving",
46+
"region_encoded",
47+
"condition_encoded",
48+
"age_group_encoded",
49+
"season_encoded",
50+
"portion_size_g",
51+
"estimated_cost_ugx",
52+
]
53+
with open(d / "xgboost_feature_names_20251103.pkl", "wb") as f:
54+
pickle.dump(feature_names, f)
55+
56+
# Create a dummy xgboost pickle model
57+
dummy = make_dummy_model(pred_value=250.0, feature_names=feature_names)
58+
with open(d / "xgboost_nutrition_model_20251103.pkl", "wb") as f:
59+
pickle.dump(dummy, f)
60+
61+
return d
62+
63+
64+
def test_local_loader_and_predict(tmp_models_dir, monkeypatch):
65+
# Disable HF snapshot during this test to avoid network calls
66+
import backend.api.models.loader as loader_mod
67+
monkeypatch.setattr(loader_mod, "HF_AVAILABLE", False)
68+
69+
# Instantiate ModelLoader pointing to our tmp models dir
70+
loader = loader_mod.ModelLoader(local_model_dir=tmp_models_dir)
71+
72+
assert loader.models.get('local_xgboost', {}).get('available') is True
73+
74+
# Prepare input matching feature names
75+
input_dict = {k: 1.0 for k in loader.feature_names}
76+
res = loader.predict(input_dict, model_preference='auto')
77+
assert res['success'] is True
78+
assert 'prediction' in res
79+
assert float(res['prediction']['caloric_needs']) == pytest.approx(250.0)
80+
81+
82+
def test_predict_endpoint_and_recommend(tmp_models_dir, monkeypatch):
83+
# Disable HF snapshot during this test
84+
import backend.api.models.loader as loader_mod
85+
monkeypatch.setattr(loader_mod, "HF_AVAILABLE", False)
86+
87+
# Create loader and attach an ensemble manually
88+
loader = loader_mod.ModelLoader(local_model_dir=tmp_models_dir)
89+
90+
# create a tiny ensemble for recommend testing
91+
emb = np.array([[1.0, 0.0], [0.0, 1.0]], dtype=float)
92+
norms = np.linalg.norm(emb, axis=1, keepdims=True)
93+
emb_norm = emb / norms
94+
loader.models['ensemble'] = {
95+
'embeddings': emb_norm,
96+
'ids': ['food_a', 'food_b'],
97+
'metadata': {'food_a': {'name': 'A'}, 'food_b': {'name': 'B'}},
98+
'available': True
99+
}
100+
101+
# Now wire this loader into the running app routers
102+
from backend.api.main import app
103+
from backend.api.routers import predict as predict_router
104+
105+
predict_router.set_model_loader(loader)
106+
107+
client = TestClient(app)
108+
109+
# Call predict endpoint
110+
payload = {k: 1.0 for k in loader.feature_names}
111+
r = client.post("/predict/", json=payload)
112+
assert r.status_code == 200
113+
body = r.json()
114+
assert body['success'] is True
115+
assert float(body['prediction']['caloric_needs']) == pytest.approx(250.0)
116+
117+
# Call recommend by id
118+
r2 = client.get('/predict/recommend', params={'by_id': 'food_a', 'top_k': 2})
119+
assert r2.status_code == 200
120+
b2 = r2.json()
121+
assert b2['success'] is True
122+
assert len(b2['items']) == 2

0 commit comments

Comments
 (0)