import pytest from models import GradingCalculator, db, Assessment, ClassGroup, Student, Exercise, GradingElement, Grade from app_config import config_manager from datetime import date class TestUnifiedGrading: """Tests pour le système de notation unifié (Phase 2 - Refactoring).""" def test_notes_calculation(self): """Test calcul notes numériques.""" # Test des valeurs numériques standard assert GradingCalculator.calculate_score('15.5', 'notes', 20) == 15.5 assert GradingCalculator.calculate_score('0', 'notes', 20) == 0.0 assert GradingCalculator.calculate_score('20', 'notes', 20) == 20.0 # Test des valeurs décimales assert GradingCalculator.calculate_score('12.75', 'notes', 20) == 12.75 def test_score_calculation(self): """Test calcul scores compétences (0-3).""" # Score 2/3 * 12 = 8.0 assert GradingCalculator.calculate_score('2', 'score', 12) == 8.0 # Score 0/3 * 20 = 0.0 assert GradingCalculator.calculate_score('0', 'score', 20) == 0.0 # Score 3/3 * 15 = 15.0 assert GradingCalculator.calculate_score('3', 'score', 15) == 15.0 # Score 1/3 * 9 = 3.0 assert GradingCalculator.calculate_score('1', 'score', 9) == 3.0 def test_special_values(self): """Test valeurs spéciales unifiées.""" # Pas de réponse = 0 assert GradingCalculator.calculate_score('.', 'notes', 20) == 0 assert GradingCalculator.calculate_score('.', 'score', 12) == 0 # Dispensé = None assert GradingCalculator.calculate_score('d', 'notes', 20) is None assert GradingCalculator.calculate_score('d', 'score', 12) is None # Absent = 0 assert GradingCalculator.calculate_score('a', 'notes', 20) == 0 assert GradingCalculator.calculate_score('a', 'score', 12) == 0 def test_is_counted_in_total(self): """Test si les valeurs comptent dans le total.""" # Valeurs normales comptent assert GradingCalculator.is_counted_in_total('15.5', 'notes') == True assert GradingCalculator.is_counted_in_total('2', 'score') == True # Pas de réponse compte (= 0) assert GradingCalculator.is_counted_in_total('.', 'notes') == True assert GradingCalculator.is_counted_in_total('.', 'score') == True # Absent compte (= 0) assert GradingCalculator.is_counted_in_total('a', 'notes') == True assert GradingCalculator.is_counted_in_total('a', 'score') == True # Dispensé ne compte pas assert GradingCalculator.is_counted_in_total('d', 'notes') == False assert GradingCalculator.is_counted_in_total('d', 'score') == False def test_validation(self): """Test validation des valeurs.""" # Notes valides assert config_manager.validate_grade_value('15.5', 'notes') == True assert config_manager.validate_grade_value('0', 'notes') == True assert config_manager.validate_grade_value('20', 'notes') == True # Scores valides assert config_manager.validate_grade_value('0', 'score') == True assert config_manager.validate_grade_value('1', 'score') == True assert config_manager.validate_grade_value('2', 'score') == True assert config_manager.validate_grade_value('3', 'score') == True # Valeurs spéciales valides assert config_manager.validate_grade_value('.', 'notes') == True assert config_manager.validate_grade_value('d', 'score') == True assert config_manager.validate_grade_value('a', 'notes') == True # Valeurs invalides assert config_manager.validate_grade_value('5', 'score') == False # > 3 assert config_manager.validate_grade_value('-1', 'notes') == False # < 0 assert config_manager.validate_grade_value('abc', 'notes') == False # non numérique assert config_manager.validate_grade_value('1.5', 'score') == False # décimal pour score def test_config_manager_methods(self): """Test nouvelles méthodes du ConfigManager.""" # Test get_grading_types types = config_manager.get_grading_types() assert 'notes' in types assert 'score' in types assert types['notes']['label'] == 'Notes numériques' assert types['score']['max_value'] == 3 # Test get_special_values special = config_manager.get_special_values() assert '.' in special assert 'd' in special assert 'a' in special assert special['.']['label'] == 'Pas de réponse' assert special['d']['counts'] == False assert special['a']['value'] == 0 # Test get_score_meanings meanings = config_manager.get_score_meanings() assert 0 in meanings assert 3 in meanings assert meanings[0]['label'] == 'Non acquis' assert meanings[3]['label'] == 'Expert' def test_display_info(self): """Test informations d'affichage.""" # Valeurs spéciales info = config_manager.get_display_info('.', 'notes') assert info['color'] == '#6b7280' assert info['label'] == 'Pas de réponse' # Scores avec significations info = config_manager.get_display_info('2', 'score') assert info['color'] == '#22c55e' assert info['label'] == 'Acquis' # Notes numériques (valeur par défaut) info = config_manager.get_display_info('15.5', 'notes') assert info['color'] == '#374151' assert info['label'] == '15.5' class TestIntegration: """Tests d'intégration pour le système unifié.""" @pytest.fixture def sample_assessment(self, app): """Fixture pour créer une évaluation de test.""" with app.app_context(): # Créer classe class_group = ClassGroup(name='6ème A', year='2025-2026') db.session.add(class_group) db.session.flush() # Créer étudiants student1 = Student(first_name='Alice', last_name='Martin', class_group_id=class_group.id) student2 = Student(first_name='Bob', last_name='Durand', class_group_id=class_group.id) db.session.add_all([student1, student2]) db.session.flush() # Créer évaluation assessment = Assessment( title='Test Unifié', date=date.today(), trimester=1, class_group_id=class_group.id ) db.session.add(assessment) db.session.flush() # Créer exercice exercise = Exercise( title='Exercice 1', assessment_id=assessment.id ) db.session.add(exercise) db.session.flush() # Créer éléments de notation avec NOUVEAUX types element_notes = GradingElement( label='Question A', exercise_id=exercise.id, max_points=20.0, grading_type='notes' # NOUVEAU type ) element_score = GradingElement( label='Compétence B', exercise_id=exercise.id, max_points=10.0, grading_type='score' # NOUVEAU type ) db.session.add_all([element_notes, element_score]) db.session.flush() # Créer notes avec NOUVEAU système grades = [ Grade(student_id=student1.id, grading_element_id=element_notes.id, value='15.5'), Grade(student_id=student1.id, grading_element_id=element_score.id, value='2'), Grade(student_id=student2.id, grading_element_id=element_notes.id, value='.'), Grade(student_id=student2.id, grading_element_id=element_score.id, value='d'), ] db.session.add_all(grades) db.session.commit() return assessment.id def test_full_assessment_workflow(self, app, sample_assessment): """Test workflow complet avec nouveaux types.""" with app.app_context(): # Récupérer l'assessment depuis la DB pour éviter les problèmes de session assessment = Assessment.query.get(sample_assessment) # Test calcul scores avec logique unifiée students_scores, exercise_scores = assessment.calculate_student_scores() # Alice : 15.5 + (2/3 * 10) = 15.5 + 6.67 = 22.17 alice_score = students_scores[1]['total_score'] # ID 1 = Alice assert alice_score == pytest.approx(22.17, rel=1e-2) # Bob : 0 + dispensé = 0 (dispensé ne compte pas dans max) bob_score = students_scores[2]['total_score'] # ID 2 = Bob assert bob_score == 0.0 # Test max points : Alice a 30 points max (20 + 10) alice_max = students_scores[1]['total_max_points'] assert alice_max == 30.0 # Bob a 20 points max (20 + dispensé ne compte pas) bob_max = students_scores[2]['total_max_points'] assert bob_max == 20.0 def test_grading_progress_calculation(self, app, sample_assessment): """Test calcul progression avec nouveaux types.""" with app.app_context(): assessment = Assessment.query.get(sample_assessment) progress = assessment.grading_progress # 2 étudiants x 2 éléments = 4 notes possibles # 4 notes saisies (y compris '.' et 'd') assert progress['total'] == 4 assert progress['completed'] == 4 assert progress['percentage'] == 100 assert progress['status'] == 'completed' def test_statistics_with_unified_system(self, app, sample_assessment): """Test statistiques avec système unifié.""" with app.app_context(): assessment = Assessment.query.get(sample_assessment) stats = assessment.get_assessment_statistics() # Vérifier calcul correct des statistiques assert stats['count'] == 2 assert stats['min'] == 0.0 # Bob assert stats['max'] == pytest.approx(22.17, rel=1e-2) # Alice # Moyenne : (22.17 + 0) / 2 = 11.085 assert stats['mean'] == pytest.approx(11.09, rel=1e-2) class TestPerformance: """Tests de performance pour le système unifié.""" def test_performance_large_dataset(self, app): """Test performance avec gros datasets (30 étudiants x 20 éléments).""" import time with app.app_context(): # Créer données de test class_group = ClassGroup(name='Grande Classe', year='2025-2026') db.session.add(class_group) db.session.flush() # 30 étudiants students = [] for i in range(30): student = Student( first_name=f'Étudiant{i}', last_name=f'Test{i}', class_group_id=class_group.id ) students.append(student) db.session.add_all(students) db.session.flush() # Évaluation avec 20 éléments assessment = Assessment( title='Test Performance', date=date.today(), trimester=1, class_group_id=class_group.id ) db.session.add(assessment) db.session.flush() exercise = Exercise(title='Exercice Performance', assessment_id=assessment.id) db.session.add(exercise) db.session.flush() # 20 éléments de notation (mix notes/scores) elements = [] for i in range(20): element_type = 'score' if i % 2 == 0 else 'notes' element = GradingElement( label=f'Élément {i}', exercise_id=exercise.id, max_points=10.0, grading_type=element_type ) elements.append(element) db.session.add_all(elements) db.session.flush() # 600 notes (30 x 20) grades = [] for student in students: for element in elements: if element.grading_type == 'score': value = str((student.id + element.id) % 4) # 0-3 else: value = str(((student.id + element.id) % 20) + 1) # 1-20 grade = Grade( student_id=student.id, grading_element_id=element.id, value=value ) grades.append(grade) db.session.add_all(grades) db.session.commit() # Test performance calcul start_time = time.time() students_scores, _ = assessment.calculate_student_scores() calculation_time = time.time() - start_time # Vérifier temps réponse < 2s assert calculation_time < 2.0, f"Calcul trop lent: {calculation_time:.2f}s" # Vérifier cohérence résultats assert len(students_scores) == 30 # Vérifier que tous les étudiants ont des scores for student_id, data in students_scores.items(): assert data['total_score'] > 0 assert data['total_max_points'] == 200.0 # 20 éléments x 10 points