Files
Classeo/backend/tests/Unit/Scolarite/Domain/Service/AverageCalculatorTest.php
Mathias STRASSER e745cf326a
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Calculer automatiquement les moyennes après chaque saisie de notes
Les enseignants ont besoin de moyennes à jour immédiatement après la
publication ou modification des notes, sans attendre un batch nocturne.

Le système recalcule via Domain Events synchrones : statistiques
d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées
(normalisation /20), et moyenne générale par élève. Les résultats sont
stockés dans des tables dénormalisées avec cache Redis (TTL 5 min).

Trois endpoints API exposent les données avec contrôle d'accès par rôle.
Une commande console permet le backfill des données historiques au
déploiement.
2026-04-02 06:45:41 +02:00

234 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Domain\Service;
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
use App\Scolarite\Domain\Service\AverageCalculator;
use App\Scolarite\Domain\Service\GradeEntry;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class AverageCalculatorTest extends TestCase
{
private AverageCalculator $calculator;
protected function setUp(): void
{
$this->calculator = new AverageCalculator();
}
// --- Subject Average ---
#[Test]
public function subjectAverageReturnsNullWhenNoGrades(): void
{
self::assertNull($this->calculator->calculateSubjectAverage([]));
}
#[Test]
public function subjectAverageWithSingleGrade(): void
{
$grades = [
new GradeEntry(
value: 15.0,
gradeScale: new GradeScale(20),
coefficient: new Coefficient(1.0),
),
];
self::assertSame(15.0, $this->calculator->calculateSubjectAverage($grades));
}
#[Test]
public function subjectAverageWithEqualCoefficients(): void
{
$grades = [
new GradeEntry(value: 12.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0)),
new GradeEntry(value: 16.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0)),
new GradeEntry(value: 8.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0)),
];
// (12 + 16 + 8) / 3 = 12.0
self::assertSame(12.0, $this->calculator->calculateSubjectAverage($grades));
}
#[Test]
public function subjectAverageWithDifferentCoefficients(): void
{
$grades = [
new GradeEntry(value: 14.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(2.0)),
new GradeEntry(value: 8.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0)),
];
// (14×2 + 8×1) / (2+1) = 36/3 = 12.0
self::assertSame(12.0, $this->calculator->calculateSubjectAverage($grades));
}
#[Test]
public function subjectAverageNormalizesToScale20(): void
{
$grades = [
new GradeEntry(value: 8.0, gradeScale: new GradeScale(10), coefficient: new Coefficient(1.0)),
];
// 8/10 × 20 = 16.0
self::assertSame(16.0, $this->calculator->calculateSubjectAverage($grades));
}
#[Test]
public function subjectAverageWithMixedScales(): void
{
$grades = [
new GradeEntry(value: 15.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0)),
new GradeEntry(value: 40.0, gradeScale: new GradeScale(100), coefficient: new Coefficient(1.0)),
];
// 15/20×20=15 et 40/100×20=8 → (15+8)/2 = 11.5
self::assertSame(11.5, $this->calculator->calculateSubjectAverage($grades));
}
#[Test]
public function subjectAverageWithMixedScalesAndCoefficients(): void
{
$grades = [
new GradeEntry(value: 16.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(3.0)),
new GradeEntry(value: 7.0, gradeScale: new GradeScale(10), coefficient: new Coefficient(2.0)),
];
// 16/20×20=16 (coef 3), 7/10×20=14 (coef 2)
// (16×3 + 14×2) / (3+2) = (48+28)/5 = 76/5 = 15.2
self::assertSame(15.2, $this->calculator->calculateSubjectAverage($grades));
}
#[Test]
public function subjectAverageRoundsToTwoDecimals(): void
{
$grades = [
new GradeEntry(value: 13.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0)),
new GradeEntry(value: 7.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0)),
new GradeEntry(value: 11.0, gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0)),
];
// (13+7+11)/3 = 31/3 = 10.333... → 10.33
self::assertSame(10.33, $this->calculator->calculateSubjectAverage($grades));
}
// --- General Average ---
#[Test]
public function generalAverageReturnsNullWhenNoSubjects(): void
{
self::assertNull($this->calculator->calculateGeneralAverage([]));
}
#[Test]
public function generalAverageWithSingleSubject(): void
{
self::assertSame(14.5, $this->calculator->calculateGeneralAverage([14.5]));
}
#[Test]
public function generalAverageIsArithmeticMean(): void
{
// (12.0 + 15.0 + 9.0) / 3 = 12.0
self::assertSame(12.0, $this->calculator->calculateGeneralAverage([12.0, 15.0, 9.0]));
}
#[Test]
public function generalAverageRoundsToTwoDecimals(): void
{
// (14.0 + 13.0 + 11.0) / 3 = 38/3 = 12.666... → 12.67
self::assertSame(12.67, $this->calculator->calculateGeneralAverage([14.0, 13.0, 11.0]));
}
// --- Class Statistics ---
#[Test]
public function classStatisticsReturnsEmptyWhenNoGrades(): void
{
$stats = $this->calculator->calculateClassStatistics([]);
self::assertNull($stats->average);
self::assertNull($stats->min);
self::assertNull($stats->max);
self::assertNull($stats->median);
self::assertSame(0, $stats->gradedCount);
}
#[Test]
public function classStatisticsWithSingleGrade(): void
{
$stats = $this->calculator->calculateClassStatistics([15.0]);
self::assertSame(15.0, $stats->average);
self::assertSame(15.0, $stats->min);
self::assertSame(15.0, $stats->max);
self::assertSame(15.0, $stats->median);
self::assertSame(1, $stats->gradedCount);
}
#[Test]
public function classStatisticsWithOddNumberOfGrades(): void
{
$stats = $this->calculator->calculateClassStatistics([8.0, 15.0, 12.0]);
// Sorted: 8, 12, 15
self::assertSame(11.67, $stats->average); // 35/3
self::assertSame(8.0, $stats->min);
self::assertSame(15.0, $stats->max);
self::assertSame(12.0, $stats->median); // middle element
self::assertSame(3, $stats->gradedCount);
}
#[Test]
public function classStatisticsWithEvenNumberOfGrades(): void
{
$stats = $this->calculator->calculateClassStatistics([7.0, 12.0, 14.0, 18.0]);
// Sorted: 7, 12, 14, 18
self::assertSame(12.75, $stats->average); // 51/4
self::assertSame(7.0, $stats->min);
self::assertSame(18.0, $stats->max);
self::assertSame(13.0, $stats->median); // (12+14)/2
self::assertSame(4, $stats->gradedCount);
}
#[Test]
public function classStatisticsSortsInputValues(): void
{
// Input not sorted
$stats = $this->calculator->calculateClassStatistics([18.0, 7.0, 14.0, 12.0]);
self::assertSame(7.0, $stats->min);
self::assertSame(18.0, $stats->max);
self::assertSame(13.0, $stats->median); // (12+14)/2
}
#[Test]
public function classStatisticsWithIdenticalGrades(): void
{
$stats = $this->calculator->calculateClassStatistics([10.0, 10.0, 10.0]);
self::assertSame(10.0, $stats->average);
self::assertSame(10.0, $stats->min);
self::assertSame(10.0, $stats->max);
self::assertSame(10.0, $stats->median);
self::assertSame(3, $stats->gradedCount);
}
#[Test]
public function classStatisticsWithTwoGrades(): void
{
$stats = $this->calculator->calculateClassStatistics([6.0, 16.0]);
self::assertSame(11.0, $stats->average);
self::assertSame(6.0, $stats->min);
self::assertSame(16.0, $stats->max);
self::assertSame(11.0, $stats->median); // (6+16)/2
self::assertSame(2, $stats->gradedCount);
}
}