""" 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()