feat: uniform competence management
This commit is contained in:
183
tests/test_config_integration.py
Normal file
183
tests/test_config_integration.py
Normal file
@@ -0,0 +1,183 @@
|
||||
import pytest
|
||||
from models import db, CompetenceScaleValue, AppConfig, Competence
|
||||
from app_config import config_manager
|
||||
|
||||
|
||||
class TestConfigIntegrationReal:
|
||||
"""Tests d'intégration réalistes pour le système de configuration."""
|
||||
|
||||
def test_auto_initialization_scale_values(self, app):
|
||||
"""Test l'initialisation automatique des valeurs d'échelle."""
|
||||
with app.app_context():
|
||||
# S'assurer que la table est vide
|
||||
CompetenceScaleValue.query.delete()
|
||||
db.session.commit()
|
||||
|
||||
# Appeler get_competence_scale_values() devrait déclencher l'initialisation
|
||||
scale_values = config_manager.get_competence_scale_values()
|
||||
|
||||
# Vérifier que les valeurs par défaut ont été créées
|
||||
assert len(scale_values) > 0
|
||||
|
||||
# Vérifier que les valeurs de base existent
|
||||
assert '0' in scale_values
|
||||
assert '1' in scale_values
|
||||
assert '2' in scale_values
|
||||
assert '3' in scale_values
|
||||
assert '.' in scale_values
|
||||
assert 'd' in scale_values
|
||||
assert 'a' in scale_values
|
||||
|
||||
# Vérifier les propriétés des valeurs
|
||||
assert scale_values['0']['label'] == 'Non acquis'
|
||||
assert scale_values['3']['label'] == 'Expert'
|
||||
assert scale_values['.']['label'] == 'Pas de réponse'
|
||||
assert scale_values['d']['included_in_total'] == False # Dispensé ne compte pas
|
||||
|
||||
def test_config_scale_page_workflow(self, app):
|
||||
"""Test le workflow complet de la page config/scale."""
|
||||
with app.app_context():
|
||||
# 1. Récupérer les valeurs initiales
|
||||
initial_values = config_manager.get_competence_scale_values()
|
||||
assert '2' in initial_values
|
||||
|
||||
# 2. Modifier une valeur existante (simuler ce que fait routes/config.py)
|
||||
success = config_manager.update_scale_value('2', 'Acquis (modifié)', '#00ff00', True)
|
||||
assert success == True
|
||||
|
||||
# 3. Vérifier que la modification a été prise en compte
|
||||
updated_values = config_manager.get_competence_scale_values()
|
||||
assert updated_values['2']['label'] == 'Acquis (modifié)'
|
||||
assert updated_values['2']['color'] == '#00ff00'
|
||||
|
||||
# 4. Ajouter une nouvelle valeur spéciale
|
||||
success = config_manager.add_scale_value('X', 'Valeur X', '#purple', False)
|
||||
assert success == True
|
||||
|
||||
# 5. Vérifier que la nouvelle valeur existe
|
||||
updated_values = config_manager.get_competence_scale_values()
|
||||
assert 'X' in updated_values
|
||||
assert updated_values['X']['label'] == 'Valeur X'
|
||||
|
||||
# 6. Supprimer la valeur ajoutée
|
||||
success = config_manager.delete_scale_value('X')
|
||||
assert success == True
|
||||
|
||||
# 7. Vérifier qu'elle a été supprimée
|
||||
final_values = config_manager.get_competence_scale_values()
|
||||
assert 'X' not in final_values
|
||||
|
||||
def test_config_validation_with_real_data(self, app):
|
||||
"""Test la validation avec les vraies données de configuration."""
|
||||
with app.app_context():
|
||||
# S'assurer que les valeurs par défaut existent
|
||||
scale_values = config_manager.get_competence_scale_values()
|
||||
|
||||
# Valeurs de score standard (0-3)
|
||||
for i in range(4):
|
||||
assert config_manager.validate_grade_value(str(i), 'score') == True
|
||||
|
||||
# Score invalide (> 3)
|
||||
assert config_manager.validate_grade_value('4', 'score') == False
|
||||
|
||||
# Valeurs spéciales
|
||||
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
|
||||
|
||||
# Notes numériques 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
|
||||
|
||||
# Notes invalides
|
||||
assert config_manager.validate_grade_value('-1', 'notes') == False
|
||||
assert config_manager.validate_grade_value('abc', 'notes') == False
|
||||
|
||||
def test_get_display_info_with_real_data(self, app):
|
||||
"""Test get_display_info avec les vraies données."""
|
||||
with app.app_context():
|
||||
# S'assurer que les valeurs existent
|
||||
scale_values = config_manager.get_competence_scale_values()
|
||||
|
||||
# Test avec une valeur de score existante
|
||||
if '2' in scale_values:
|
||||
info = config_manager.get_display_info('2', 'score')
|
||||
assert info['label'] == scale_values['2']['label']
|
||||
assert info['color'] == scale_values['2']['color']
|
||||
|
||||
# Test avec valeur spéciale
|
||||
if '.' in scale_values:
|
||||
info = config_manager.get_display_info('.', 'notes')
|
||||
assert info['label'] == scale_values['.']['label']
|
||||
assert info['color'] == scale_values['.']['color']
|
||||
|
||||
# Test avec note numérique (valeur par défaut)
|
||||
info = config_manager.get_display_info('15.5', 'notes')
|
||||
assert info['label'] == '15.5'
|
||||
assert info['color'] == '#374151' # Couleur par défaut
|
||||
|
||||
def test_route_compatibility(self, app):
|
||||
"""Test la compatibilité avec les routes existantes."""
|
||||
with app.app_context():
|
||||
# Test du format attendu par la route config.scale
|
||||
scale_values = config_manager.get_competence_scale_values()
|
||||
|
||||
# Vérifier le format de données
|
||||
for value, config in scale_values.items():
|
||||
assert isinstance(value, str) # Clé doit être string
|
||||
assert 'label' in config
|
||||
assert 'color' in config
|
||||
assert 'included_in_total' in config
|
||||
assert isinstance(config['included_in_total'], bool)
|
||||
|
||||
# Vérifier format couleur
|
||||
color = config['color']
|
||||
assert color.startswith('#')
|
||||
assert len(color) == 7 # Format #RRGGBB
|
||||
|
||||
# Test des opérations CRUD utilisées par les routes
|
||||
original_count = len(scale_values)
|
||||
|
||||
# Ajouter (utilisé par routes/config.py:add_scale_value)
|
||||
success = config_manager.add_scale_value('TEST', 'Test Value', '#123456', True)
|
||||
assert success == True
|
||||
assert len(config_manager.get_competence_scale_values()) == original_count + 1
|
||||
|
||||
# Modifier (utilisé par routes/config.py:update_scale)
|
||||
success = config_manager.update_scale_value('TEST', 'Modified Test', '#654321', False)
|
||||
assert success == True
|
||||
updated_values = config_manager.get_competence_scale_values()
|
||||
assert updated_values['TEST']['label'] == 'Modified Test'
|
||||
assert updated_values['TEST']['color'] == '#654321'
|
||||
assert updated_values['TEST']['included_in_total'] == False
|
||||
|
||||
# Supprimer (utilisé par routes/config.py:delete_scale_value)
|
||||
success = config_manager.delete_scale_value('TEST')
|
||||
assert success == True
|
||||
assert len(config_manager.get_competence_scale_values()) == original_count
|
||||
|
||||
def test_grading_system_integration(self, app):
|
||||
"""Test l'intégration avec le système de notation unifié."""
|
||||
with app.app_context():
|
||||
# Vérifier que les types de notation sont bien définis
|
||||
grading_types = config_manager.get_grading_types()
|
||||
assert 'notes' in grading_types
|
||||
assert 'score' in grading_types
|
||||
|
||||
# Vérifier que les valeurs spéciales sont cohérentes
|
||||
special_values = config_manager.get_special_values()
|
||||
scale_values = config_manager.get_competence_scale_values()
|
||||
|
||||
# Les valeurs spéciales doivent exister dans l'échelle
|
||||
for special_key in special_values.keys():
|
||||
assert special_key in scale_values
|
||||
|
||||
# Vérifier les significations des scores
|
||||
score_meanings = config_manager.get_score_meanings()
|
||||
for score in range(4): # 0, 1, 2, 3
|
||||
assert score in score_meanings
|
||||
score_str = str(score)
|
||||
if score_str in scale_values:
|
||||
# Le label de l'échelle doit correspondre à la signification
|
||||
assert scale_values[score_str]['label'] == score_meanings[score]['label']
|
||||
364
tests/test_config_system.py
Normal file
364
tests/test_config_system.py
Normal file
@@ -0,0 +1,364 @@
|
||||
import pytest
|
||||
from models import db, CompetenceScaleValue, AppConfig, Competence
|
||||
from app_config import config_manager
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TestConfigManager:
|
||||
"""Tests pour le ConfigManager et la gestion de la configuration."""
|
||||
|
||||
def test_get_grading_types(self, app):
|
||||
"""Test récupération des types de notation."""
|
||||
with app.app_context():
|
||||
grading_types = config_manager.get_grading_types()
|
||||
|
||||
assert 'notes' in grading_types
|
||||
assert 'score' in grading_types
|
||||
assert grading_types['notes']['label'] == 'Notes numériques'
|
||||
assert grading_types['score']['max_value'] == 3
|
||||
|
||||
def test_get_special_values(self, app):
|
||||
"""Test récupération des valeurs spéciales."""
|
||||
with app.app_context():
|
||||
special_values = config_manager.get_special_values()
|
||||
|
||||
assert '.' in special_values
|
||||
assert 'd' in special_values
|
||||
assert 'a' in special_values
|
||||
assert special_values['.']['label'] == 'Pas de réponse'
|
||||
assert special_values['d']['counts'] == False
|
||||
assert special_values['a']['value'] == 0
|
||||
|
||||
def test_get_score_meanings(self, app):
|
||||
"""Test récupération des significations des scores."""
|
||||
with app.app_context():
|
||||
score_meanings = config_manager.get_score_meanings()
|
||||
|
||||
assert 0 in score_meanings
|
||||
assert 1 in score_meanings
|
||||
assert 2 in score_meanings
|
||||
assert 3 in score_meanings
|
||||
assert score_meanings[0]['label'] == 'Non acquis'
|
||||
assert score_meanings[3]['label'] == 'Expert'
|
||||
|
||||
def test_validate_grade_value(self, app):
|
||||
"""Test validation des valeurs de notation."""
|
||||
with app.app_context():
|
||||
# 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
|
||||
assert config_manager.validate_grade_value('-1', 'notes') == False
|
||||
assert config_manager.validate_grade_value('abc', 'notes') == False
|
||||
assert config_manager.validate_grade_value('1.5', 'score') == False
|
||||
|
||||
def test_get_display_info(self, app):
|
||||
"""Test informations d'affichage."""
|
||||
with app.app_context():
|
||||
# Valeur spéciale
|
||||
info = config_manager.get_display_info('.', 'notes')
|
||||
assert 'color' in info
|
||||
assert 'label' in info
|
||||
assert info['label'] == 'Pas de réponse'
|
||||
|
||||
# Score avec signification
|
||||
info = config_manager.get_display_info('2', 'score')
|
||||
assert info['label'] == 'Acquis'
|
||||
|
||||
# Note numérique
|
||||
info = config_manager.get_display_info('15.5', 'notes')
|
||||
assert info['label'] == '15.5'
|
||||
|
||||
|
||||
class TestCompetenceScaleValue:
|
||||
"""Tests pour le modèle CompetenceScaleValue."""
|
||||
|
||||
def test_create_scale_value(self, app):
|
||||
"""Test création d'une valeur d'échelle."""
|
||||
with app.app_context():
|
||||
scale_value = CompetenceScaleValue(
|
||||
value='test',
|
||||
label='Test Value',
|
||||
color='#ff0000',
|
||||
included_in_total=True
|
||||
)
|
||||
db.session.add(scale_value)
|
||||
db.session.commit()
|
||||
|
||||
# Vérifier que la valeur a été créée
|
||||
saved_value = CompetenceScaleValue.query.get('test')
|
||||
assert saved_value is not None
|
||||
assert saved_value.label == 'Test Value'
|
||||
assert saved_value.color == '#ff0000'
|
||||
assert saved_value.included_in_total == True
|
||||
|
||||
def test_scale_value_primary_key(self, app):
|
||||
"""Test que 'value' est bien la clé primaire."""
|
||||
with app.app_context():
|
||||
scale_value1 = CompetenceScaleValue(
|
||||
value='unique',
|
||||
label='First',
|
||||
color='#ff0000'
|
||||
)
|
||||
scale_value2 = CompetenceScaleValue(
|
||||
value='unique',
|
||||
label='Second',
|
||||
color='#00ff00'
|
||||
)
|
||||
|
||||
db.session.add(scale_value1)
|
||||
db.session.commit()
|
||||
|
||||
# Essayer d'ajouter une valeur avec la même clé primaire
|
||||
db.session.add(scale_value2)
|
||||
with pytest.raises(Exception):
|
||||
db.session.commit()
|
||||
|
||||
def test_get_competence_scale_values(self, app):
|
||||
"""Test récupération des valeurs d'échelle."""
|
||||
with app.app_context():
|
||||
# Nettoyer la table d'abord
|
||||
CompetenceScaleValue.query.delete()
|
||||
db.session.commit()
|
||||
|
||||
# Ajouter quelques valeurs de test
|
||||
values = [
|
||||
CompetenceScaleValue(value='0', label='Zéro', color='#ff0000'),
|
||||
CompetenceScaleValue(value='1', label='Un', color='#ffff00'),
|
||||
CompetenceScaleValue(value='2', label='Deux', color='#00ff00'),
|
||||
]
|
||||
for value in values:
|
||||
db.session.add(value)
|
||||
db.session.commit()
|
||||
|
||||
# Récupérer via config_manager
|
||||
scale_values = config_manager.get_competence_scale_values()
|
||||
|
||||
# Les clés doivent être des strings
|
||||
assert '0' in scale_values
|
||||
assert '1' in scale_values
|
||||
assert '2' in scale_values
|
||||
assert scale_values['0']['label'] == 'Zéro'
|
||||
assert scale_values['1']['color'] == '#ffff00'
|
||||
|
||||
|
||||
class TestConfigOperations:
|
||||
"""Tests pour les opérations CRUD sur la configuration."""
|
||||
|
||||
def test_add_scale_value(self, app):
|
||||
"""Test ajout d'une valeur d'échelle."""
|
||||
with app.app_context():
|
||||
# Ajouter une nouvelle valeur
|
||||
success = config_manager.add_scale_value('X', 'Valeur X', '#123456', False)
|
||||
assert success == True
|
||||
|
||||
# Vérifier qu'elle a été ajoutée
|
||||
scale_value = CompetenceScaleValue.query.get('X')
|
||||
assert scale_value is not None
|
||||
assert scale_value.label == 'Valeur X'
|
||||
assert scale_value.color == '#123456'
|
||||
assert scale_value.included_in_total == False
|
||||
|
||||
def test_update_scale_value(self, app):
|
||||
"""Test mise à jour d'une valeur d'échelle."""
|
||||
with app.app_context():
|
||||
# Créer une valeur
|
||||
scale_value = CompetenceScaleValue(
|
||||
value='update_test',
|
||||
label='Original',
|
||||
color='#000000',
|
||||
included_in_total=True
|
||||
)
|
||||
db.session.add(scale_value)
|
||||
db.session.commit()
|
||||
|
||||
# Mettre à jour
|
||||
success = config_manager.update_scale_value('update_test', 'Updated', '#ffffff', False)
|
||||
assert success == True
|
||||
|
||||
# Vérifier la mise à jour
|
||||
updated_value = CompetenceScaleValue.query.get('update_test')
|
||||
assert updated_value.label == 'Updated'
|
||||
assert updated_value.color == '#ffffff'
|
||||
assert updated_value.included_in_total == False
|
||||
|
||||
def test_update_nonexistent_scale_value(self, app):
|
||||
"""Test mise à jour d'une valeur qui n'existe pas."""
|
||||
with app.app_context():
|
||||
success = config_manager.update_scale_value('nonexistent', 'Test', '#000000', True)
|
||||
assert success == False
|
||||
|
||||
def test_delete_scale_value(self, app):
|
||||
"""Test suppression d'une valeur d'échelle."""
|
||||
with app.app_context():
|
||||
# Créer une valeur
|
||||
scale_value = CompetenceScaleValue(
|
||||
value='delete_test',
|
||||
label='To Delete',
|
||||
color='#000000'
|
||||
)
|
||||
db.session.add(scale_value)
|
||||
db.session.commit()
|
||||
|
||||
# Vérifier qu'elle existe
|
||||
assert CompetenceScaleValue.query.get('delete_test') is not None
|
||||
|
||||
# Supprimer
|
||||
success = config_manager.delete_scale_value('delete_test')
|
||||
assert success == True
|
||||
|
||||
# Vérifier qu'elle a été supprimée
|
||||
assert CompetenceScaleValue.query.get('delete_test') is None
|
||||
|
||||
def test_delete_nonexistent_scale_value(self, app):
|
||||
"""Test suppression d'une valeur qui n'existe pas."""
|
||||
with app.app_context():
|
||||
success = config_manager.delete_scale_value('nonexistent')
|
||||
assert success == False
|
||||
|
||||
|
||||
class TestConfigIntegration:
|
||||
"""Tests d'intégration pour le système de configuration."""
|
||||
|
||||
@pytest.fixture
|
||||
def setup_scale_values(self, app):
|
||||
"""Fixture pour créer des valeurs d'échelle de test."""
|
||||
with app.app_context():
|
||||
values = [
|
||||
CompetenceScaleValue(value='0', label='Non acquis', color='#ef4444', included_in_total=True),
|
||||
CompetenceScaleValue(value='1', label='En cours', color='#f59e0b', included_in_total=True),
|
||||
CompetenceScaleValue(value='2', label='Acquis', color='#22c55e', included_in_total=True),
|
||||
CompetenceScaleValue(value='3', label='Expert', color='#3b82f6', included_in_total=True),
|
||||
CompetenceScaleValue(value='.', label='Pas de réponse', color='#6b7280', included_in_total=True),
|
||||
CompetenceScaleValue(value='d', label='Dispensé', color='#9ca3af', included_in_total=False),
|
||||
CompetenceScaleValue(value='a', label='Absent', color='#f87171', included_in_total=True),
|
||||
]
|
||||
for value in values:
|
||||
db.session.add(value)
|
||||
db.session.commit()
|
||||
return values
|
||||
|
||||
def test_full_scale_workflow(self, app, setup_scale_values):
|
||||
"""Test workflow complet de gestion d'échelle."""
|
||||
with app.app_context():
|
||||
# 1. Récupérer les valeurs
|
||||
scale_values = config_manager.get_competence_scale_values()
|
||||
assert len(scale_values) == 7
|
||||
|
||||
# 2. Ajouter une nouvelle valeur
|
||||
success = config_manager.add_scale_value('N', 'Non évalué', '#cccccc', False)
|
||||
assert success == True
|
||||
|
||||
scale_values = config_manager.get_competence_scale_values()
|
||||
assert len(scale_values) == 8
|
||||
assert 'N' in scale_values
|
||||
|
||||
# 3. Modifier une valeur existante
|
||||
success = config_manager.update_scale_value('N', 'Non évalué (modifié)', '#dddddd', True)
|
||||
assert success == True
|
||||
|
||||
updated_value = CompetenceScaleValue.query.get('N')
|
||||
assert updated_value.label == 'Non évalué (modifié)'
|
||||
assert updated_value.included_in_total == True
|
||||
|
||||
# 4. Supprimer la valeur
|
||||
success = config_manager.delete_scale_value('N')
|
||||
assert success == True
|
||||
|
||||
scale_values = config_manager.get_competence_scale_values()
|
||||
assert len(scale_values) == 7
|
||||
assert 'N' not in scale_values
|
||||
|
||||
def test_validation_with_database_values(self, app, setup_scale_values):
|
||||
"""Test validation avec les valeurs de la base de données."""
|
||||
with app.app_context():
|
||||
# Valeurs numériques des scores
|
||||
assert config_manager.validate_grade_value('0', 'score') == True
|
||||
assert config_manager.validate_grade_value('3', 'score') == True
|
||||
assert config_manager.validate_grade_value('4', 'score') == False # Au-dessus du max
|
||||
|
||||
# Valeurs spéciales
|
||||
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 inexistantes
|
||||
assert config_manager.validate_grade_value('Z', 'score') == False
|
||||
|
||||
def test_display_info_with_database_values(self, app, setup_scale_values):
|
||||
"""Test informations d'affichage avec valeurs de la base."""
|
||||
with app.app_context():
|
||||
# Score avec signification
|
||||
info = config_manager.get_display_info('2', 'score')
|
||||
assert info['label'] == 'Acquis'
|
||||
assert info['color'] == '#22c55e'
|
||||
|
||||
# Valeur spéciale
|
||||
info = config_manager.get_display_info('.', 'notes')
|
||||
assert info['label'] == 'Pas de réponse'
|
||||
assert info['color'] == '#6b7280'
|
||||
|
||||
# Valeur inexistante (devrait retourner la valeur par défaut)
|
||||
info = config_manager.get_display_info('99', 'notes')
|
||||
assert info['label'] == '99' # Valeur brute
|
||||
assert info['color'] == '#374151' # Couleur par défaut
|
||||
|
||||
|
||||
class TestConfigErrorHandling:
|
||||
"""Tests de gestion d'erreurs pour la configuration."""
|
||||
|
||||
def test_add_duplicate_scale_value(self, app):
|
||||
"""Test ajout d'une valeur d'échelle en double."""
|
||||
with app.app_context():
|
||||
# Ajouter une valeur
|
||||
success1 = config_manager.add_scale_value('dup', 'Original', '#000000')
|
||||
assert success1 == True
|
||||
|
||||
# Essayer d'ajouter la même valeur
|
||||
success2 = config_manager.add_scale_value('dup', 'Duplicate', '#ffffff')
|
||||
assert success2 == False
|
||||
|
||||
def test_invalid_color_format(self, app):
|
||||
"""Test avec format de couleur invalide."""
|
||||
with app.app_context():
|
||||
# Ces tests dépendent de la validation dans les routes,
|
||||
# mais on peut tester le comportement du modèle
|
||||
scale_value = CompetenceScaleValue(
|
||||
value='invalid_color',
|
||||
label='Test',
|
||||
color='invalid' # Format invalide mais le modèle l'accepte
|
||||
)
|
||||
db.session.add(scale_value)
|
||||
db.session.commit()
|
||||
|
||||
# Le modèle accepte n'importe quelle string, la validation doit être faite côté route
|
||||
saved_value = CompetenceScaleValue.query.get('invalid_color')
|
||||
assert saved_value.color == 'invalid'
|
||||
|
||||
def test_empty_label(self, app):
|
||||
"""Test avec libellé vide."""
|
||||
with app.app_context():
|
||||
scale_value = CompetenceScaleValue(
|
||||
value='empty_label',
|
||||
label='', # Libellé vide
|
||||
color='#000000'
|
||||
)
|
||||
db.session.add(scale_value)
|
||||
db.session.commit()
|
||||
|
||||
saved_value = CompetenceScaleValue.query.get('empty_label')
|
||||
assert saved_value.label == ''
|
||||
@@ -235,7 +235,7 @@ class TestGradingElement:
|
||||
description="Calculer 1/2 + 1/3",
|
||||
skill="Additionner des fractions",
|
||||
max_points=4.0,
|
||||
grading_type="points"
|
||||
grading_type="notes"
|
||||
)
|
||||
db.session.add(grading_element)
|
||||
db.session.commit()
|
||||
@@ -243,7 +243,7 @@ class TestGradingElement:
|
||||
assert grading_element.id is not None
|
||||
assert grading_element.label == "Question 1"
|
||||
assert grading_element.max_points == 4.0
|
||||
assert grading_element.grading_type == "points"
|
||||
assert grading_element.grading_type == "notes"
|
||||
|
||||
def test_grading_element_default_type(self, app):
|
||||
with app.app_context():
|
||||
@@ -268,7 +268,7 @@ class TestGradingElement:
|
||||
# Default value is set in the column definition, check after saving
|
||||
db.session.add(grading_element)
|
||||
db.session.commit()
|
||||
assert grading_element.grading_type == "points"
|
||||
assert grading_element.grading_type == "notes"
|
||||
|
||||
def test_grading_element_repr(self, app):
|
||||
with app.app_context():
|
||||
|
||||
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