335 lines
14 KiB
Python
335 lines
14 KiB
Python
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 |