File size: 9,739 Bytes
408204a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | """
Tests for GestureScorer β the quality scoring system.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β π CONCEPT: Unit Testing with unittest β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β β
β Python's built-in unittest module provides everything we need: β
β β
β β’ unittest.TestCase β Base class for test classes β
β β’ self.assertEqual() β Check two values are equal β
β β’ self.assertTrue() β Check a condition is True β
β β’ self.assertIsNone() β Check a value is None β
β β’ self.assertGreater() β Check a > b β
β β’ self.assertAlmostEqual() β Check floats are close (avoids rounding) β
β β
β Each test method name must start with "test_". β
β Each test checks ONE specific behavior. β
β β
β HOW TO RUN: β
β python -m unittest tests.test_scoring -v β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
"""
import unittest
from oop_sorting_teaching.models.gesture import GestureImage, GestureRanking
from oop_sorting_teaching.models.scoring import GestureScorer
# ==============================================================================
# Helper: Create a GestureImage with known values
# ==============================================================================
def make_gesture(name: str, capture_id: int, confidence: float) -> GestureImage:
"""
Factory helper for creating test gestures.
Using a helper keeps tests short and focused on WHAT is tested,
not on HOW to construct objects.
"""
return GestureImage.create_from_prediction(
gesture_name=name,
capture_id=capture_id,
confidence=confidence,
)
# ==============================================================================
# Tests for compute_score
# ==============================================================================
class TestComputeScore(unittest.TestCase):
"""
Tests for GestureScorer.compute_score().
π OOP NOTE:
By inheriting from unittest.TestCase, our class gains
all the assertion methods (assertEqual, assertTrue, etc.)
without writing them ourselves. This is INHERITANCE in action!
"""
def test_basic_fist(self):
"""Fist (complexity 1.0) at 90% confidence β score = 90.0."""
gesture = make_gesture("fist", 1, confidence=0.90)
score = GestureScorer.compute_score(gesture)
self.assertEqual(score, 90.0)
def test_basic_ok(self):
"""OK (complexity 1.7) at 80% confidence β score = 136.0."""
gesture = make_gesture("ok", 2, confidence=0.80)
score = GestureScorer.compute_score(gesture)
self.assertEqual(score, 136.0)
def test_zero_confidence(self):
"""Zero confidence should always produce a score of 0."""
gesture = make_gesture("call", 3, confidence=0.0)
score = GestureScorer.compute_score(gesture)
self.assertEqual(score, 0.0)
def test_full_confidence_fist(self):
"""100% confidence on fist (complexity 1.0) β exactly 100."""
gesture = make_gesture("fist", 4, confidence=1.0)
score = GestureScorer.compute_score(gesture)
self.assertEqual(score, 100.0)
def test_mute_highest_complexity(self):
"""Mute (complexity 2.0) at 100% confidence β 200.0."""
gesture = make_gesture("mute", 5, confidence=1.0)
score = GestureScorer.compute_score(gesture)
self.assertEqual(score, 200.0)
def test_confidence_weight_doubles_score(self):
"""Doubling confidence_weight should double the score."""
gesture = make_gesture("fist", 6, confidence=0.50)
normal = GestureScorer.compute_score(gesture, confidence_weight=1.0)
doubled = GestureScorer.compute_score(gesture, confidence_weight=2.0)
# assertAlmostEqual handles floating-point precision gracefully
self.assertAlmostEqual(doubled, normal * 2, places=5)
def test_complexity_weight_changes_score(self):
"""Higher complexity_weight should amplify complexity differences."""
gesture = make_gesture("ok", 7, confidence=1.0)
# complexity for ok = 1.7
low_weight = GestureScorer.compute_score(
gesture, complexity_weight=0.5
)
high_weight = GestureScorer.compute_score(
gesture, complexity_weight=2.0
)
# 1.7^0.5 β 1.304, 1.7^2.0 = 2.89 β high > low
self.assertGreater(high_weight, low_weight)
def test_score_is_rounded(self):
"""Score should be rounded to 2 decimal places."""
gesture = make_gesture("rock", 8, confidence=0.777)
score = GestureScorer.compute_score(gesture)
# 0.777 * 100 * 1.6 = 124.32
self.assertEqual(score, 124.32)
# ==============================================================================
# Tests for apply_scores and clear_scores
# ==============================================================================
class TestApplyAndClearScores(unittest.TestCase):
"""Tests for applying and clearing scores on gesture lists."""
def test_apply_scores_sets_sort_value(self):
"""After apply_scores, every gesture should have _sort_value set."""
images = [
make_gesture("fist", 1, 0.9),
make_gesture("ok", 2, 0.8),
make_gesture("call", 3, 0.7),
]
GestureScorer.apply_scores(images)
for img in images:
self.assertIsNotNone(img._sort_value)
def test_apply_scores_negates(self):
"""_sort_value should be negative (for descending sort order)."""
images = [make_gesture("fist", 1, 0.9)]
GestureScorer.apply_scores(images)
self.assertLess(images[0]._sort_value, 0)
def test_apply_scores_correct_value(self):
"""_sort_value should equal the negated computed score."""
gesture = make_gesture("fist", 1, 0.9)
expected_score = GestureScorer.compute_score(gesture)
GestureScorer.apply_scores([gesture])
self.assertAlmostEqual(gesture._sort_value, -expected_score, places=5)
def test_clear_scores_resets_to_none(self):
"""After clear_scores, _sort_value should be None."""
images = [
make_gesture("fist", 1, 0.9),
make_gesture("ok", 2, 0.8),
]
GestureScorer.apply_scores(images)
GestureScorer.clear_scores(images)
for img in images:
self.assertIsNone(img._sort_value)
def test_get_score_returns_positive(self):
"""get_score should return the positive (display) score."""
gesture = make_gesture("ok", 1, 0.8)
GestureScorer.apply_scores([gesture])
display_score = GestureScorer.get_score(gesture)
self.assertGreater(display_score, 0)
self.assertEqual(display_score, GestureScorer.compute_score(gesture))
def test_get_score_returns_zero_when_no_score(self):
"""get_score returns 0.0 when no score has been applied."""
gesture = make_gesture("fist", 1, 0.9)
self.assertEqual(GestureScorer.get_score(gesture), 0.0)
# ==============================================================================
# Tests for get_formula_description
# ==============================================================================
class TestFormulaDescription(unittest.TestCase):
"""Tests for the human-readable formula string."""
def test_default_formula(self):
"""Default weights produce a readable formula string."""
desc = GestureScorer.get_formula_description()
self.assertIn("confidence", desc)
self.assertIn("complexity", desc)
self.assertIn("1.0", desc)
def test_custom_weights_in_formula(self):
"""Custom weights appear in the formula description."""
desc = GestureScorer.get_formula_description(2.5, 1.5)
self.assertIn("2.5", desc)
self.assertIn("1.5", desc)
# ==============================================================================
# Run tests when this file is executed directly
# ==============================================================================
# π This is the standard Python way to make a script runnable:
# python tests/test_scoring.py β runs all tests in this file
# python -m unittest tests.test_scoring -v β same, with verbose output
if __name__ == "__main__":
unittest.main()
|