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.
234 lines
7.7 KiB
PHP
234 lines
7.7 KiB
PHP
<?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);
|
||
}
|
||
}
|