""" Tests de validation pour la migration Pattern Strategy (JOUR 3-4). Ce module teste que l'implémentation avec Pattern Strategy donne exactement les mêmes résultats que l'implémentation legacy, garantissant ainsi une migration sans régression. """ import pytest from decimal import Decimal from config.feature_flags import feature_flags, FeatureFlag from models import GradingCalculator class TestPatternStrategyMigration: """ Tests de validation pour s'assurer que la migration vers le Pattern Strategy ne change aucun comportement existant. """ def setup_method(self): """Préparation avant chaque test.""" # S'assurer que le flag est désactivé au début feature_flags.disable(FeatureFlag.USE_STRATEGY_PATTERN, "Test setup") def teardown_method(self): """Nettoyage après chaque test.""" # Remettre le flag à l'état désactivé feature_flags.disable(FeatureFlag.USE_STRATEGY_PATTERN, "Test teardown") def test_calculate_score_notes_identical_results(self): """ Test que les calculs de notes donnent des résultats identiques entre l'implémentation legacy et la nouvelle. """ test_cases = [ ("15.5", "notes", 20.0, 15.5), ("0", "notes", 20.0, 0.0), ("20", "notes", 20.0, 20.0), ("10.25", "notes", 20.0, 10.25), ("invalid", "notes", 20.0, 0.0), ] for grade_value, grading_type, max_points, expected in test_cases: # Test avec implémentation legacy feature_flags.disable(FeatureFlag.USE_STRATEGY_PATTERN, "Testing legacy") legacy_result = GradingCalculator.calculate_score(grade_value, grading_type, max_points) # Test avec nouvelle implémentation feature_flags.enable(FeatureFlag.USE_STRATEGY_PATTERN, "Testing new strategy") strategy_result = GradingCalculator.calculate_score(grade_value, grading_type, max_points) # Les résultats doivent être identiques assert legacy_result == strategy_result, ( f"Résultats différents pour {grade_value}: " f"legacy={legacy_result}, strategy={strategy_result}" ) assert legacy_result == expected def test_calculate_score_score_identical_results(self): """ Test que les calculs de scores (0-3) donnent des résultats identiques. """ test_cases = [ ("0", "score", 12.0, 0.0), ("1", "score", 12.0, 4.0), # (1/3) * 12 = 4 ("2", "score", 12.0, 8.0), # (2/3) * 12 = 8 ("3", "score", 12.0, 12.0), # (3/3) * 12 = 12 ("invalid", "score", 12.0, 0.0), ("4", "score", 12.0, 0.0), # Invalide, hors limite ] for grade_value, grading_type, max_points, expected in test_cases: # Test avec implémentation legacy feature_flags.disable(FeatureFlag.USE_STRATEGY_PATTERN, "Testing legacy") legacy_result = GradingCalculator.calculate_score(grade_value, grading_type, max_points) # Test avec nouvelle implémentation feature_flags.enable(FeatureFlag.USE_STRATEGY_PATTERN, "Testing new strategy") strategy_result = GradingCalculator.calculate_score(grade_value, grading_type, max_points) # Les résultats doivent être identiques assert legacy_result == strategy_result, ( f"Résultats différents pour {grade_value}: " f"legacy={legacy_result}, strategy={strategy_result}" ) assert abs(legacy_result - expected) < 0.001 # Tolérance pour les floats def test_special_values_identical_results(self, app): """ Test que les valeurs spéciales sont traitées identiquement. Nécessite l'application Flask pour l'accès à la configuration. """ with app.app_context(): # Valeurs spéciales courantes special_cases = [ (".", "notes", 20.0), # Pas de réponse -> 0 ("d", "notes", 20.0), # Dispensé -> None (".", "score", 12.0), # Pas de réponse -> 0 ("d", "score", 12.0), # Dispensé -> None ] for grade_value, grading_type, max_points in special_cases: # Test avec implémentation legacy feature_flags.disable(FeatureFlag.USE_STRATEGY_PATTERN, "Testing legacy") legacy_result = GradingCalculator.calculate_score(grade_value, grading_type, max_points) # Test avec nouvelle implémentation feature_flags.enable(FeatureFlag.USE_STRATEGY_PATTERN, "Testing new strategy") strategy_result = GradingCalculator.calculate_score(grade_value, grading_type, max_points) # Les résultats doivent être identiques assert legacy_result == strategy_result, ( f"Résultats différents pour valeur spéciale {grade_value}: " f"legacy={legacy_result}, strategy={strategy_result}" ) def test_is_counted_in_total_identical_results(self, app): """ Test que is_counted_in_total donne des résultats identiques. """ with app.app_context(): test_cases = [ ("15.5", "notes", True), # Valeur normale (".", "notes", True), # Pas de réponse compte dans le total ("d", "notes", False), # Dispensé ne compte pas ("0", "score", True), # Valeur normale (".", "score", True), # Pas de réponse compte dans le total ("d", "score", False), # Dispensé ne compte pas ] for grade_value, grading_type, expected in test_cases: # Test avec implémentation legacy feature_flags.disable(FeatureFlag.USE_STRATEGY_PATTERN, "Testing legacy") legacy_result = GradingCalculator.is_counted_in_total(grade_value, grading_type) # Test avec nouvelle implémentation feature_flags.enable(FeatureFlag.USE_STRATEGY_PATTERN, "Testing new strategy") strategy_result = GradingCalculator.is_counted_in_total(grade_value, grading_type) # Les résultats doivent être identiques assert legacy_result == strategy_result, ( f"Résultats différents pour is_counted_in_total {grade_value}: " f"legacy={legacy_result}, strategy={strategy_result}" ) assert legacy_result == expected def test_feature_flag_toggle_works_correctly(self): """ Test que le basculement du feature flag fonctionne correctement. """ grade_value, grading_type, max_points = "15.5", "notes", 20.0 # Vérifier état initial (désactivé) assert not feature_flags.is_enabled(FeatureFlag.USE_STRATEGY_PATTERN) result_disabled = GradingCalculator.calculate_score(grade_value, grading_type, max_points) # Activer le flag feature_flags.enable(FeatureFlag.USE_STRATEGY_PATTERN, "Test toggle") assert feature_flags.is_enabled(FeatureFlag.USE_STRATEGY_PATTERN) result_enabled = GradingCalculator.calculate_score(grade_value, grading_type, max_points) # Désactiver le flag feature_flags.disable(FeatureFlag.USE_STRATEGY_PATTERN, "Test toggle back") assert not feature_flags.is_enabled(FeatureFlag.USE_STRATEGY_PATTERN) result_disabled_again = GradingCalculator.calculate_score(grade_value, grading_type, max_points) # Tous les résultats doivent être identiques assert result_disabled == result_enabled == result_disabled_again assert result_disabled == 15.5 def test_strategy_pattern_performance_acceptable(self): """ Test que la nouvelle implémentation n'a pas de dégradation majeure de performance. """ import time grade_value, grading_type, max_points = "15.5", "notes", 20.0 iterations = 1000 # Mesure performance legacy feature_flags.disable(FeatureFlag.USE_STRATEGY_PATTERN, "Performance test legacy") start_legacy = time.time() for _ in range(iterations): GradingCalculator.calculate_score(grade_value, grading_type, max_points) time_legacy = time.time() - start_legacy # Mesure performance strategy feature_flags.enable(FeatureFlag.USE_STRATEGY_PATTERN, "Performance test strategy") start_strategy = time.time() for _ in range(iterations): GradingCalculator.calculate_score(grade_value, grading_type, max_points) time_strategy = time.time() - start_strategy # La nouvelle implémentation ne doit pas être plus de 3x plus lente performance_ratio = time_strategy / time_legacy assert performance_ratio < 3.0, ( f"Performance dégradée: strategy={time_strategy:.4f}s, " f"legacy={time_legacy:.4f}s, ratio={performance_ratio:.2f}" ) class TestPatternStrategyFactoryValidation: """Tests de validation de la factory des strategies.""" def test_strategy_factory_creates_correct_strategies(self): """Test que la factory crée les bonnes strategies.""" from services.assessment_services import GradingStrategyFactory # Strategy pour notes notes_strategy = GradingStrategyFactory.create('notes') assert notes_strategy.get_grading_type() == 'notes' # Strategy pour scores score_strategy = GradingStrategyFactory.create('score') assert score_strategy.get_grading_type() == 'score' # Type invalide with pytest.raises(ValueError, match="Type de notation non supporté"): GradingStrategyFactory.create('invalid_type') def test_strategy_patterns_work_correctly(self): """Test que les strategies individuelles fonctionnent correctement.""" from services.assessment_services import GradingStrategyFactory # Test NotesStrategy notes_strategy = GradingStrategyFactory.create('notes') assert notes_strategy.calculate_score("15.5", 20.0) == 15.5 assert notes_strategy.calculate_score("invalid", 20.0) == 0.0 # Test ScoreStrategy score_strategy = GradingStrategyFactory.create('score') assert score_strategy.calculate_score("2", 12.0) == 8.0 # (2/3) * 12 assert score_strategy.calculate_score("invalid", 12.0) == 0.0 if __name__ == "__main__": pytest.main([__file__, "-v"])