feat: preparing migration
This commit is contained in:
332
tests/test_assessment_services.py
Normal file
332
tests/test_assessment_services.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user