Files
notytex/tests/test_assessment_services.py

332 lines
12 KiB
Python

"""
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 FlaskConfigProvider, 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)