feat: uniform competence management
This commit is contained in:
335
tests/test_unified_grading.py
Normal file
335
tests/test_unified_grading.py
Normal file
@@ -0,0 +1,335 @@
|
||||
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
|
||||
Reference in New Issue
Block a user