""" Tests pour les services d'évaluation refactorisés. Ce module teste la nouvelle architecture avec injection de dépendances et s'assure de la rétrocompatibilité avec l'API existante. """ import pytest from unittest.mock import Mock, MagicMock from dataclasses import asdict from services.assessment_services import ( AssessmentServicesFacade, AssessmentProgressService, StudentScoreCalculator, AssessmentStatisticsService, UnifiedGradingCalculator, GradingStrategyFactory, NotesStrategy, ScoreStrategy, ProgressResult, StudentScore, StatisticsResult ) from providers.concrete_providers import ConfigManagerProvider, SQLAlchemyDatabaseProvider class TestGradingStrategies: """Test du pattern Strategy pour les types de notation.""" def test_notes_strategy(self): strategy = NotesStrategy() assert strategy.calculate_score('15.5', 20.0) == 15.5 assert strategy.calculate_score('0', 20.0) == 0.0 assert strategy.calculate_score('invalid', 20.0) == 0.0 assert strategy.get_grading_type() == 'notes' def test_score_strategy(self): strategy = ScoreStrategy() assert strategy.calculate_score('0', 3.0) == 0.0 assert strategy.calculate_score('1', 3.0) == 1.0 assert strategy.calculate_score('2', 3.0) == 2.0 assert strategy.calculate_score('3', 3.0) == 3.0 assert strategy.calculate_score('4', 3.0) == 0.0 # Hors limites assert strategy.calculate_score('invalid', 3.0) == 0.0 assert strategy.get_grading_type() == 'score' def test_strategy_factory(self): notes_strategy = GradingStrategyFactory.create('notes') score_strategy = GradingStrategyFactory.create('score') assert isinstance(notes_strategy, NotesStrategy) assert isinstance(score_strategy, ScoreStrategy) with pytest.raises(ValueError, match="Type de notation non supporté"): GradingStrategyFactory.create('invalid') def test_strategy_extensibility(self): """Test que le factory peut être étendu avec de nouveaux types.""" class CustomStrategy: def calculate_score(self, grade_value, max_points): return 42.0 def get_grading_type(self): return 'custom' GradingStrategyFactory.register_strategy('custom', CustomStrategy) custom_strategy = GradingStrategyFactory.create('custom') assert isinstance(custom_strategy, CustomStrategy) assert custom_strategy.calculate_score('any', 10) == 42.0 class TestUnifiedGradingCalculator: """Test du calculateur unifié avec injection de dépendances.""" def test_calculate_score_with_special_values(self): # Mock du config provider config_provider = Mock() config_provider.is_special_value.return_value = True config_provider.get_special_values.return_value = { '.': {'value': 0, 'counts': True}, 'd': {'value': None, 'counts': False} # Dispensé } calculator = UnifiedGradingCalculator(config_provider) # Test valeur spéciale "." assert calculator.calculate_score('.', 'notes', 20.0) == 0.0 # Test valeur spéciale "d" (dispensé) assert calculator.calculate_score('d', 'notes', 20.0) is None def test_calculate_score_normal_values(self): # Mock du config provider pour valeurs normales config_provider = Mock() config_provider.is_special_value.return_value = False calculator = UnifiedGradingCalculator(config_provider) # Test notes normales assert calculator.calculate_score('15.5', 'notes', 20.0) == 15.5 # Test scores normaux assert calculator.calculate_score('2', 'score', 3.0) == 2.0 def test_is_counted_in_total(self): config_provider = Mock() config_provider.is_special_value.return_value = True config_provider.get_special_values.return_value = { '.': {'counts': True}, 'd': {'counts': False} } calculator = UnifiedGradingCalculator(config_provider) assert calculator.is_counted_in_total('.') == True assert calculator.is_counted_in_total('d') == False class TestAssessmentProgressService: """Test du service de progression.""" def test_calculate_grading_progress_no_students(self): # Mock de l'assessment sans étudiants assessment = Mock() assessment.class_group.students = [] db_provider = Mock() service = AssessmentProgressService(db_provider) result = service.calculate_grading_progress(assessment) assert result.percentage == 0 assert result.status == 'no_students' assert result.students_count == 0 def test_calculate_grading_progress_normal(self): # Mock de l'assessment avec étudiants assessment = Mock() assessment.id = 1 assessment.class_group.students = [Mock(), Mock()] # 2 étudiants # Mock du provider de données db_provider = Mock() db_provider.get_grading_elements_with_students.return_value = [ {'completed_grades_count': 1}, # Élément 1: 1/2 complété {'completed_grades_count': 2} # Élément 2: 2/2 complété ] service = AssessmentProgressService(db_provider) result = service.calculate_grading_progress(assessment) # 3 notes complétées sur 4 possibles = 75% assert result.percentage == 75 assert result.completed == 3 assert result.total == 4 assert result.status == 'in_progress' assert result.students_count == 2 class TestStudentScoreCalculator: """Test du calculateur de scores étudiants.""" def test_calculate_student_scores(self): # Configuration des mocks grading_calculator = Mock() grading_calculator.calculate_score.return_value = 10.0 grading_calculator.is_counted_in_total.return_value = True db_provider = Mock() db_provider.get_grades_for_assessment.return_value = [ { 'student_id': 1, 'grading_element_id': 1, 'value': '10', 'grading_type': 'notes', 'max_points': 20.0 } ] # Mock de l'assessment student = Mock() student.id = 1 student.first_name = 'Jean' student.last_name = 'Dupont' exercise = Mock() exercise.id = 1 exercise.title = 'Exercice 1' exercise.grading_elements = [Mock()] exercise.grading_elements[0].id = 1 exercise.grading_elements[0].max_points = 20.0 exercise.grading_elements[0].grading_type = 'notes' assessment = Mock() assessment.id = 1 assessment.class_group.students = [student] assessment.exercises = [exercise] calculator = StudentScoreCalculator(grading_calculator, db_provider) students_scores, exercise_scores = calculator.calculate_student_scores(assessment) assert len(students_scores) == 1 assert 1 in students_scores student_score = students_scores[1] assert student_score.student_id == 1 assert student_score.student_name == 'Jean Dupont' assert student_score.total_score == 10.0 class TestAssessmentStatisticsService: """Test du service de statistiques.""" def test_get_assessment_statistics_no_scores(self): score_calculator = Mock() score_calculator.calculate_student_scores.return_value = ({}, {}) service = AssessmentStatisticsService(score_calculator) assessment = Mock() result = service.get_assessment_statistics(assessment) assert result.count == 0 assert result.mean == 0 assert result.median == 0 def test_get_assessment_statistics_with_scores(self): # Mock des scores étudiants mock_scores = { 1: StudentScore(1, 'Student 1', 15.0, 20.0, {}), 2: StudentScore(2, 'Student 2', 18.0, 20.0, {}), 3: StudentScore(3, 'Student 3', 12.0, 20.0, {}) } score_calculator = Mock() score_calculator.calculate_student_scores.return_value = (mock_scores, {}) service = AssessmentStatisticsService(score_calculator) assessment = Mock() result = service.get_assessment_statistics(assessment) assert result.count == 3 assert result.mean == 15.0 # (15+18+12)/3 assert result.median == 15.0 assert result.min == 12.0 assert result.max == 18.0 class TestAssessmentServicesFacade: """Test de la facade qui regroupe tous les services.""" def test_facade_integration(self): config_provider = Mock() config_provider.is_special_value.return_value = False db_provider = Mock() db_provider.get_grading_elements_with_students.return_value = [] db_provider.get_grades_for_assessment.return_value = [] facade = AssessmentServicesFacade(config_provider, db_provider) # Vérifier que tous les services sont disponibles assert hasattr(facade, 'grading_calculator') assert hasattr(facade, 'progress_service') assert hasattr(facade, 'score_calculator') assert hasattr(facade, 'statistics_service') # Test des méthodes de la facade assessment = Mock() assessment.id = 1 assessment.class_group.students = [] progress = facade.get_grading_progress(assessment) assert isinstance(progress, ProgressResult) students, exercises = facade.calculate_student_scores(assessment) assert isinstance(students, dict) assert isinstance(exercises, dict) stats = facade.get_statistics(assessment) assert isinstance(stats, StatisticsResult) class TestRegressionCompatibility: """Tests de régression pour s'assurer de la rétrocompatibilité.""" def test_grading_progress_api_compatibility(self): """S'assurer que l'API grading_progress reste identique.""" config_provider = Mock() db_provider = Mock() db_provider.get_grading_elements_with_students.return_value = [ {'completed_grades_count': 5} ] facade = AssessmentServicesFacade(config_provider, db_provider) assessment = Mock() assessment.id = 1 assessment.class_group.students = [Mock(), Mock()] # 2 étudiants progress = facade.get_grading_progress(assessment) # L'API originale retournait un dict, vérifions les clés expected_keys = {'percentage', 'completed', 'total', 'status', 'students_count'} actual_dict = asdict(progress) assert set(actual_dict.keys()) == expected_keys def test_calculate_student_scores_api_compatibility(self): """S'assurer que calculate_student_scores garde la même signature.""" config_provider = Mock() config_provider.is_special_value.return_value = False db_provider = Mock() db_provider.get_grades_for_assessment.return_value = [] facade = AssessmentServicesFacade(config_provider, db_provider) assessment = Mock() assessment.id = 1 assessment.class_group.students = [] assessment.exercises = [] students_scores, exercise_scores = facade.calculate_student_scores(assessment) # L'API originale retournait un tuple de 2 dicts assert isinstance(students_scores, dict) assert isinstance(exercise_scores, dict)