Skip to content

Commit 27b823c

Browse files
authored
Update test_predict.py
1 parent 788370e commit 27b823c

1 file changed

Lines changed: 31 additions & 40 deletions

File tree

tests/test_predict.py

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11

22
import json
33
import os
4-
import tempfile
54
from pathlib import Path
6-
from unittest.mock import MagicMock, patch
5+
from unittest.mock import MagicMock
76

87
import numpy as np
98
import pytest
@@ -14,13 +13,13 @@
1413

1514
@pytest.fixture
1615
def dummy_image_path(tmp_path):
17-
"""Create a 224x224 dummy RGB image for testing — no real leaf needed."""
16+
"""224x224 dummy RGB image — no real leaf needed."""
1817
img = Image.fromarray(
1918
np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
2019
)
21-
img_path = tmp_path / "test_leaf.jpg"
22-
img.save(img_path)
23-
return str(img_path)
20+
path = tmp_path / "test_leaf.jpg"
21+
img.save(path)
22+
return str(path)
2423

2524

2625
@pytest.fixture
@@ -39,33 +38,33 @@ def dummy_class_names():
3938

4039
@pytest.fixture
4140
def mock_model(dummy_class_names):
42-
"""Mock Keras model that returns a valid softmax-like output (sums to 1)."""
41+
"""Mock Keras model returns valid softmax output. No TF needed."""
4342
model = MagicMock()
44-
num_classes = len(dummy_class_names)
45-
raw = np.random.rand(1, num_classes).astype(np.float32)
46-
raw = raw / raw.sum() # normalise to sum to 1
43+
n = len(dummy_class_names)
44+
raw = np.random.rand(1, n).astype(np.float32)
45+
raw = raw / raw.sum()
4746
model.predict.return_value = raw
4847
return model
4948

5049

5150
@pytest.fixture
5251
def high_confidence_mock_model(dummy_class_names):
53-
"""Mock model that always returns very high confidence on class 0."""
52+
"""Mock model that always returns 99% confidence on class 0."""
5453
model = MagicMock()
55-
num_classes = len(dummy_class_names)
56-
scores = np.zeros((1, num_classes), dtype=np.float32)
54+
n = len(dummy_class_names)
55+
scores = np.zeros((1, n), dtype=np.float32)
5756
scores[0, 0] = 0.99
58-
scores[0, 1:] = 0.01 / (num_classes - 1)
57+
scores[0, 1:] = 0.01 / (n - 1)
5958
model.predict.return_value = scores
6059
return model
6160

6261

6362
@pytest.fixture
6463
def low_confidence_mock_model(dummy_class_names):
65-
"""Mock model that returns nearly uniform (low confidence) output."""
64+
"""Mock model that returns uniform (low confidence) output."""
6665
model = MagicMock()
67-
num_classes = len(dummy_class_names)
68-
scores = np.full((1, num_classes), 1.0 / num_classes, dtype=np.float32)
66+
n = len(dummy_class_names)
67+
scores = np.full((1, n), 1.0 / n, dtype=np.float32)
6968
model.predict.return_value = scores
7069
return model
7170

@@ -76,9 +75,7 @@ class TestPreprocessImage:
7675
def test_output_shape(self, dummy_image_path):
7776
from src.predict import preprocess_image
7877
result = preprocess_image(dummy_image_path)
79-
assert result.shape == (1, 224, 224, 3), (
80-
f"Expected (1, 224, 224, 3), got {result.shape}"
81-
)
78+
assert result.shape == (1, 224, 224, 3)
8279

8380
def test_output_dtype_is_float32(self, dummy_image_path):
8481
from src.predict import preprocess_image
@@ -92,40 +89,38 @@ def test_pixel_values_in_0_255_range(self, dummy_image_path):
9289
assert result.min() >= 0.0
9390
assert result.max() <= 255.0
9491

95-
def test_handles_non_rgb_image(self, tmp_path):
96-
"""RGBA images should be converted to RGB without crashing."""
92+
def test_handles_rgba_image(self, tmp_path):
93+
"""RGBA images must be converted to RGB without crashing."""
9794
from src.predict import preprocess_image
9895
img = Image.fromarray(
9996
np.random.randint(0, 255, (100, 100, 4), dtype=np.uint8), mode="RGBA"
10097
)
101-
img_path = str(tmp_path / "rgba.png")
102-
img.save(img_path)
103-
result = preprocess_image(img_path)
98+
path = str(tmp_path / "rgba.png")
99+
img.save(path)
100+
result = preprocess_image(path)
104101
assert result.shape == (1, 224, 224, 3)
105102

106103

107104
# ── predict ────────────────────────────────────────────────────────────────────
108105

109106
class TestPredict:
110-
def test_returns_dict_with_required_keys(self, dummy_image_path, mock_model, dummy_class_names):
107+
def test_returns_required_keys(self, dummy_image_path, mock_model, dummy_class_names):
111108
from src.predict import predict
112109
result = predict(dummy_image_path, model=mock_model, class_names=dummy_class_names)
113110
for key in ("predicted_class", "confidence", "top3", "all_scores", "low_confidence"):
114-
assert key in result, f"Missing key: {key}"
111+
assert key in result
115112

116113
def test_predicted_class_is_valid_or_unknown(self, dummy_image_path, mock_model, dummy_class_names):
117114
from src.predict import predict
118115
result = predict(dummy_image_path, model=mock_model, class_names=dummy_class_names)
119116
assert result["predicted_class"] in dummy_class_names or result["predicted_class"] == "Unknown"
120117

121-
def test_confidence_between_0_and_1(self, dummy_image_path, mock_model, dummy_class_names):
118+
def test_confidence_in_range(self, dummy_image_path, mock_model, dummy_class_names):
122119
from src.predict import predict
123120
result = predict(dummy_image_path, model=mock_model, class_names=dummy_class_names)
124-
assert 0.0 <= result["confidence"] <= 1.0, (
125-
f"Confidence {result['confidence']} out of [0, 1]"
126-
)
121+
assert 0.0 <= result["confidence"] <= 1.0
127122

128-
def test_top3_has_exactly_three_items(self, dummy_image_path, mock_model, dummy_class_names):
123+
def test_top3_length(self, dummy_image_path, mock_model, dummy_class_names):
129124
from src.predict import predict
130125
result = predict(dummy_image_path, model=mock_model, class_names=dummy_class_names)
131126
assert len(result["top3"]) == 3
@@ -134,7 +129,7 @@ def test_top3_sorted_descending(self, dummy_image_path, mock_model, dummy_class_
134129
from src.predict import predict
135130
result = predict(dummy_image_path, model=mock_model, class_names=dummy_class_names)
136131
confs = [item["confidence"] for item in result["top3"]]
137-
assert confs == sorted(confs, reverse=True), "Top-3 must be sorted highest → lowest"
132+
assert confs == sorted(confs, reverse=True)
138133

139134
def test_all_scores_has_all_classes(self, dummy_image_path, mock_model, dummy_class_names):
140135
from src.predict import predict
@@ -147,8 +142,7 @@ def test_high_confidence_not_flagged(self, dummy_image_path, high_confidence_moc
147142
assert result["low_confidence"] is False
148143
assert result["predicted_class"] != "Unknown"
149144

150-
def test_low_confidence_flagged_as_unknown(self, dummy_image_path, low_confidence_mock_model, dummy_class_names):
151-
"""When confidence is below threshold, predicted_class must be 'Unknown'."""
145+
def test_low_confidence_returns_unknown(self, dummy_image_path, low_confidence_mock_model, dummy_class_names):
152146
from src.predict import predict
153147
result = predict(dummy_image_path, model=low_confidence_mock_model, class_names=dummy_class_names)
154148
assert result["low_confidence"] is True
@@ -160,11 +154,10 @@ def test_missing_image_raises_error(self, mock_model, dummy_class_names):
160154
predict("/nonexistent/path/leaf.jpg", model=mock_model, class_names=dummy_class_names)
161155

162156

163-
# ── model building (weights mocked to avoid 29MB download in CI) ──────────────
157+
# ── model building (no TF import at module level — lazy inside functions) ──────
164158

165159
class TestModelBuilding:
166160
def test_model_output_shape(self):
167-
"""FIX: use load_weights=False to skip ImageNet download in CI/tests."""
168161
from src.model import build_model
169162
model = build_model(num_classes=8, load_weights=False)
170163
assert model.output_shape == (None, 8)
@@ -175,7 +168,6 @@ def test_model_input_shape(self):
175168
assert model.input_shape == (None, 224, 224, 3)
176169

177170
def test_model_compiles_without_error(self):
178-
import tensorflow as tf
179171
from src.model import build_model
180172
model = build_model(num_classes=8, load_weights=False)
181173
model.compile(
@@ -188,7 +180,7 @@ def test_custom_num_classes(self):
188180
from src.model import build_model
189181
for n in [2, 10, 38]:
190182
model = build_model(num_classes=n, load_weights=False)
191-
assert model.output_shape == (None, n), f"Failed for num_classes={n}"
183+
assert model.output_shape == (None, n)
192184

193185
def test_custom_input_shape(self):
194186
from src.model import build_model
@@ -209,7 +201,6 @@ def test_missing_model_raises_file_not_found(self, tmp_path):
209201

210202
def test_missing_class_names_raises_file_not_found(self, tmp_path):
211203
from src.predict import load_model_and_classes
212-
# Create a dummy model file so the first check passes
213204
fake_model = tmp_path / "model.keras"
214205
fake_model.write_text("fake")
215206
with pytest.raises(FileNotFoundError, match="Class names not found"):

0 commit comments

Comments
 (0)