""" Tests pour la migration de get_assessment_statistics() vers AssessmentStatisticsService. Cette étape 3.2 de migration valide que : 1. Les calculs statistiques sont identiques (legacy vs refactored) 2. Les performances sont maintenues ou améliorées 3. L'interface reste compatible (format dict inchangé) 4. Le feature flag USE_REFACTORED_ASSESSMENT contrôle la migration """ import pytest from unittest.mock import patch import time from models import Assessment, ClassGroup, Student, Exercise, GradingElement, Grade, db from config.feature_flags import FeatureFlag from app_config import config_manager class TestAssessmentStatisticsMigration: def test_statistics_migration_flag_off_uses_legacy(self, app): """ RÈGLE MÉTIER : Quand le feature flag USE_REFACTORED_ASSESSMENT est désactivé, get_assessment_statistics() doit utiliser la version legacy. """ with app.app_context(): # Désactiver le feature flag config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) # Créer des données de test assessment = self._create_assessment_with_scores() # Mock pour s'assurer que les services refactorisés ne sont pas appelés with patch('services.assessment_services.create_assessment_services') as mock_services: stats = assessment.get_assessment_statistics() # Les services refactorisés ne doivent PAS être appelés mock_services.assert_not_called() # Vérifier le format de retour assert isinstance(stats, dict) assert 'count' in stats assert 'mean' in stats assert 'median' in stats assert 'min' in stats assert 'max' in stats assert 'std_dev' in stats def test_statistics_migration_flag_on_uses_refactored(self, app): """ RÈGLE MÉTIER : Quand le feature flag USE_REFACTORED_ASSESSMENT est activé, get_assessment_statistics() doit utiliser les services refactorisés. """ with app.app_context(): # Activer le feature flag config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', True) try: # Créer des données de test assessment = self._create_assessment_with_scores() # Appeler la méthode stats = assessment.get_assessment_statistics() # Vérifier le format de retour (identique au legacy) assert isinstance(stats, dict) assert 'count' in stats assert 'mean' in stats assert 'median' in stats assert 'min' in stats assert 'max' in stats assert 'std_dev' in stats # Vérifier que les valeurs sont cohérentes assert stats['count'] == 3 # 3 étudiants assert stats['mean'] > 0 assert stats['median'] > 0 assert stats['min'] <= stats['mean'] <= stats['max'] assert stats['std_dev'] >= 0 finally: # Remettre le flag par défaut config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) def test_statistics_results_identical_legacy_vs_refactored(self, app): """ RÈGLE CRITIQUE : Les résultats calculés par la version legacy et refactored doivent être EXACTEMENT identiques. """ with app.app_context(): # Créer des données de test complexes assessment = self._create_complex_assessment_with_scores() # Test avec flag OFF (legacy) config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) legacy_stats = assessment.get_assessment_statistics() # Test avec flag ON (refactored) config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', True) try: refactored_stats = assessment.get_assessment_statistics() # Comparaison exacte assert legacy_stats['count'] == refactored_stats['count'] assert legacy_stats['mean'] == refactored_stats['mean'] assert legacy_stats['median'] == refactored_stats['median'] assert legacy_stats['min'] == refactored_stats['min'] assert legacy_stats['max'] == refactored_stats['max'] assert legacy_stats['std_dev'] == refactored_stats['std_dev'] # Test d'identité complète assert legacy_stats == refactored_stats finally: config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) def test_statistics_empty_assessment_both_versions(self, app): """ Test des cas limites : évaluation sans notes. """ with app.app_context(): # Créer une évaluation sans notes class_group = ClassGroup(name="Test Class", year="2025-2026") db.session.add(class_group) db.session.commit() assessment = Assessment( title="Test Assessment", description="Test Description", date=None, class_group_id=class_group.id, trimester=1 ) db.session.add(assessment) db.session.commit() # Test legacy config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) legacy_stats = assessment.get_assessment_statistics() # Test refactored config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', True) try: refactored_stats = assessment.get_assessment_statistics() # Vérifier que les deux versions gèrent correctement le cas vide expected_empty = { 'count': 0, 'mean': 0, 'median': 0, 'min': 0, 'max': 0, 'std_dev': 0 } assert legacy_stats == expected_empty assert refactored_stats == expected_empty finally: config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) def test_statistics_performance_comparison(self, app): """ PERFORMANCE : Vérifier que la version refactored n'est pas plus lente. """ with app.app_context(): # Créer une évaluation avec beaucoup de données assessment = self._create_large_assessment_with_scores() # Mesurer le temps legacy config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) start_time = time.perf_counter() legacy_stats = assessment.get_assessment_statistics() legacy_time = time.perf_counter() - start_time # Mesurer le temps refactored config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', True) try: start_time = time.perf_counter() refactored_stats = assessment.get_assessment_statistics() refactored_time = time.perf_counter() - start_time # Les résultats doivent être identiques assert legacy_stats == refactored_stats # La version refactored ne doit pas être 2x plus lente assert refactored_time <= legacy_time * 2, ( f"Refactored trop lent: {refactored_time:.4f}s vs Legacy: {legacy_time:.4f}s" ) print(f"Performance comparison - Legacy: {legacy_time:.4f}s, Refactored: {refactored_time:.4f}s") finally: config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) def test_statistics_integration_with_results_page(self, app, client): """ Test d'intégration : la page de résultats doit fonctionner avec les deux versions. """ with app.app_context(): assessment = self._create_assessment_with_scores() # Test avec legacy config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) response = client.get(f'/assessments/{assessment.id}/results') assert response.status_code == 200 assert b'Statistiques' in response.data # Vérifier que les stats s'affichent # Test avec refactored config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', True) try: response = client.get(f'/assessments/{assessment.id}/results') assert response.status_code == 200 assert b'Statistiques' in response.data # Vérifier que les stats s'affichent finally: config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) # === Méthodes utilitaires === def _create_assessment_with_scores(self): """Crée une évaluation simple avec quelques scores.""" # Classe et étudiants class_group = ClassGroup(name="Test Class", year="2025-2026") db.session.add(class_group) db.session.commit() students = [ Student(first_name="Alice", last_name="Dupont", class_group_id=class_group.id), Student(first_name="Bob", last_name="Martin", class_group_id=class_group.id), Student(first_name="Charlie", last_name="Durand", class_group_id=class_group.id) ] for student in students: db.session.add(student) db.session.commit() # Évaluation assessment = Assessment( title="Test Assessment", description="Test Description", date=None, class_group_id=class_group.id, trimester=1 ) db.session.add(assessment) db.session.commit() # Exercice exercise = Exercise( title="Exercise 1", assessment_id=assessment.id, ) db.session.add(exercise) db.session.commit() # Éléments de notation element = GradingElement( label="Question 1", exercise_id=exercise.id, max_points=20, grading_type="notes", ) db.session.add(element) db.session.commit() # Notes grades = [ Grade(student_id=students[0].id, grading_element_id=element.id, value="15"), Grade(student_id=students[1].id, grading_element_id=element.id, value="18"), Grade(student_id=students[2].id, grading_element_id=element.id, value="12") ] for grade in grades: db.session.add(grade) db.session.commit() return assessment def _create_complex_assessment_with_scores(self): """Crée une évaluation complexe avec différents types de scores.""" # Classe et étudiants class_group = ClassGroup(name="Complex Class", year="2025-2026") db.session.add(class_group) db.session.commit() students = [ Student(first_name="Alice", last_name="Dupont", class_group_id=class_group.id), Student(first_name="Bob", last_name="Martin", class_group_id=class_group.id), Student(first_name="Charlie", last_name="Durand", class_group_id=class_group.id), Student(first_name="Diana", last_name="Petit", class_group_id=class_group.id) ] for student in students: db.session.add(student) db.session.commit() # Évaluation assessment = Assessment( title="Complex Assessment", description="Test Description", date=None, class_group_id=class_group.id, trimester=1 ) db.session.add(assessment) db.session.commit() # Exercice 1 - Notes exercise1 = Exercise( title="Exercise Points", assessment_id=assessment.id, ) db.session.add(exercise1) db.session.commit() element1 = GradingElement( label="Question Points", exercise_id=exercise1.id, max_points=20, grading_type="notes", ) db.session.add(element1) db.session.commit() # Exercice 2 - Scores exercise2 = Exercise( title="Exercise Competences", assessment_id=assessment.id, order=2 ) db.session.add(exercise2) db.session.commit() element2 = GradingElement( label="Competence", exercise_id=exercise2.id, max_points=3, grading_type="score", ) db.session.add(element2) db.session.commit() # Notes variées avec cas spéciaux grades = [ # Étudiant 1 - bonnes notes Grade(student_id=students[0].id, grading_element_id=element1.id, value="18"), Grade(student_id=students[0].id, grading_element_id=element2.id, value="3"), # Étudiant 2 - notes moyennes Grade(student_id=students[1].id, grading_element_id=element1.id, value="14"), Grade(student_id=students[1].id, grading_element_id=element2.id, value="2"), # Étudiant 3 - notes faibles avec cas spécial Grade(student_id=students[2].id, grading_element_id=element1.id, value="8"), Grade(student_id=students[2].id, grading_element_id=element2.id, value="."), # Pas de réponse # Étudiant 4 - dispensé Grade(student_id=students[3].id, grading_element_id=element1.id, value="d"), # Dispensé Grade(student_id=students[3].id, grading_element_id=element2.id, value="1"), ] for grade in grades: db.session.add(grade) db.session.commit() return assessment def _create_large_assessment_with_scores(self): """Crée une évaluation avec beaucoup de données pour les tests de performance.""" # Classe et étudiants class_group = ClassGroup(name="Large Class", year="2025-2026") db.session.add(class_group) db.session.commit() # Créer 20 étudiants students = [] for i in range(20): student = Student( first_name=f"Student{i}", last_name=f"Test{i}", class_group_id=class_group.id ) students.append(student) db.session.add(student) db.session.commit() # Évaluation assessment = Assessment( title="Large Assessment", description="Performance test", date=None, class_group_id=class_group.id, trimester=1 ) db.session.add(assessment) db.session.commit() # Créer 5 exercices avec plusieurs éléments for ex_num in range(5): exercise = Exercise( title=f"Exercise {ex_num + 1}", assessment_id=assessment.id, ) db.session.add(exercise) db.session.commit() # 3 éléments par exercice for elem_num in range(3): element = GradingElement( label=f"Question {elem_num + 1}", exercise_id=exercise.id, max_points=10, grading_type="notes", ) db.session.add(element) db.session.commit() # Notes pour tous les étudiants for student in students: score = 5 + (i + ex_num + elem_num) % 6 # Scores variés entre 5 et 10 grade = Grade( student_id=student.id, grading_element_id=element.id, value=str(score) ) db.session.add(grade) db.session.commit() return assessment