CI / build-and-test (push) Canceled after 0s
Neuer Assistent (Einstieg im Zeugnisnoten-Dialog) geht die Lerngruppe Schüler für Schüler durch: berechnete Note mit ungerundetem Wert und Teilbereichen, Mitarbeitsgraph abschnittsweise oder als Verlauf, Arbeiten und Einzelnoten. Das 1. Halbjahr erscheint im 2. Halbjahr nur als Kontext. Die Endjahresnote wird jetzt in Dialog und Assistent aus den Halbjahresnoten verrechnet (gemeinsamer ReportGradeCalculator). In Klasse 10 kommen Projektprüfung, schriftliche/mündliche Abschlussprüfung und zusätzliche mündliche Prüfung hinzu, gewichtet nach Oberschulverordnung und PrüfV Sek. I; gerundet wird dabei bewusst nicht automatisch. Die Zeitleisten-Erzeugung des Mitarbeits-Assistenten liegt dafür im gemeinsamen ParticipationTimelineBuilder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
357 lines
13 KiB
C#
357 lines
13 KiB
C#
using LehrerApp.Core.Models;
|
|
using LehrerApp.Core.Services;
|
|
using Xunit;
|
|
|
|
namespace LehrerApp.Tests;
|
|
|
|
public class GradingServiceTests
|
|
{
|
|
private readonly GradingService _sut = new();
|
|
|
|
// ── CalculateGrade: Notenschlüssel-Grenzfälle ────────────────────────────
|
|
|
|
[Theory]
|
|
[InlineData(87.5, 100, "1")] // exakt an der Grenze zählt für die bessere Note
|
|
[InlineData(87.4, 100, "2")] // knapp darunter
|
|
[InlineData(100, 100, "1")] // Maximalpunktzahl
|
|
[InlineData(50, 100, "4")]
|
|
[InlineData(0, 100, "6")] // Minimum, exakt an der untersten Grenze
|
|
public void CalculateGrade_Grades1To6_Grenzwerte(double achieved, double maximum, string expected)
|
|
{
|
|
var result = _sut.CalculateGrade(achieved, maximum, GradingService.DefaultKey1To6());
|
|
Assert.Equal(expected, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateGrade_MaximumNull_GibtPlatzhalterZurueck()
|
|
{
|
|
var result = _sut.CalculateGrade(5, 0, GradingService.DefaultKey1To6());
|
|
Assert.Equal("-", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateGrade_MaximumNegativ_GibtPlatzhalterZurueck()
|
|
{
|
|
var result = _sut.CalculateGrade(5, -10, GradingService.DefaultKey1To6());
|
|
Assert.Equal("-", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateGrade_UeberschreitetMaximum_GibtBesteNote()
|
|
{
|
|
// z.B. Bonuspunkte über die maximale Punktzahl hinaus
|
|
var result = _sut.CalculateGrade(120, 100, GradingService.DefaultKey1To6());
|
|
Assert.Equal("1", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateGrade_NegativeErreichtePunkte_FaelltAufSchlechtesteNoteZurueck()
|
|
{
|
|
// Kein Schlüsseleintrag deckt einen negativen Prozentsatz ab -> Fallback auf die
|
|
// Stufe mit der niedrigsten Grenze (schlechteste Note), nicht auf einen Crash.
|
|
var result = _sut.CalculateGrade(-5, 100, GradingService.DefaultKey1To6());
|
|
Assert.Equal("6", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateGrade_Punkte0Bis15_Grenzwerte()
|
|
{
|
|
var key = GradingService.DefaultKey0To15();
|
|
Assert.Equal("15", _sut.CalculateGrade(95, 100, key));
|
|
Assert.Equal("14", _sut.CalculateGrade(94.9, 100, key));
|
|
Assert.Equal("0", _sut.CalculateGrade(0, 100, key));
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateGrade_UnsortierterSchluessel_FunktioniertGenauso()
|
|
{
|
|
var shuffled = GradingService.DefaultKey1To6().OrderBy(_ => Guid.NewGuid()).ToList();
|
|
var result = _sut.CalculateGrade(80, 100, shuffled);
|
|
Assert.Equal("2", result);
|
|
}
|
|
|
|
// ── ValidateGradingKey ────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ValidateGradingKey_Standardschluessel1To6_IstGueltig()
|
|
{
|
|
Assert.Null(_sut.ValidateGradingKey(GradingService.DefaultKey1To6()));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateGradingKey_Standardschluessel0To15_IstGueltig()
|
|
{
|
|
Assert.Null(_sut.ValidateGradingKey(GradingService.DefaultKey0To15()));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateGradingKey_WenigerAlsZweiStufen_LiefertFehler()
|
|
{
|
|
var error = _sut.ValidateGradingKey([new GradingKeyEntry { Grade = "1", MinPercent = 0 }]);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateGradingKey_LeereBezeichnung_LiefertFehler()
|
|
{
|
|
var error = _sut.ValidateGradingKey(
|
|
[
|
|
new() { Grade = "", MinPercent = 50 },
|
|
new() { Grade = "6", MinPercent = 0 },
|
|
]);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1)]
|
|
[InlineData(101)]
|
|
public void ValidateGradingKey_ProzentgrenzeAusserhalbBereich_LiefertFehler(double minPercent)
|
|
{
|
|
var error = _sut.ValidateGradingKey(
|
|
[
|
|
new() { Grade = "1", MinPercent = minPercent },
|
|
new() { Grade = "6", MinPercent = 0 },
|
|
]);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateGradingKey_DoppelteProzentgrenze_LiefertFehler()
|
|
{
|
|
var error = _sut.ValidateGradingKey(
|
|
[
|
|
new() { Grade = "1", MinPercent = 50 },
|
|
new() { Grade = "2", MinPercent = 50 },
|
|
new() { Grade = "6", MinPercent = 0 },
|
|
]);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateGradingKey_UntersteStufeNichtBeiNull_LiefertFehler()
|
|
{
|
|
var error = _sut.ValidateGradingKey(
|
|
[
|
|
new() { Grade = "1", MinPercent = 50 },
|
|
new() { Grade = "6", MinPercent = 10 },
|
|
]);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
// ── WeightedAverage: gewichteter Schnitt ─────────────────────────────────
|
|
|
|
[Fact]
|
|
public void WeightedAverage_GleicheGewichte_EntsprichtEinfachemMittel()
|
|
{
|
|
var result = _sut.WeightedAverage([("2", 1.0), ("4", 1.0)]);
|
|
Assert.Equal(3.0, result, precision: 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedAverage_UnterschiedlicheGewichte_GewichtetKorrekt()
|
|
{
|
|
// Note 1 zählt doppelt so stark wie Note 4 -> (1*2 + 4*1) / 3 = 2
|
|
var result = _sut.WeightedAverage([("1", 2.0), ("4", 1.0)]);
|
|
Assert.Equal(2.0, result, precision: 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedAverage_NichtNumerischeWerte_WerdenIgnoriert()
|
|
{
|
|
var result = _sut.WeightedAverage([("2", 1.0), ("entfällt", 1.0), ("4", 1.0)]);
|
|
Assert.Equal(3.0, result, precision: 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedAverage_LeereListe_GibtNullZurueck()
|
|
{
|
|
Assert.Equal(0, _sut.WeightedAverage([]));
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedAverage_NurNichtNumerischeWerte_GibtNullZurueck()
|
|
{
|
|
Assert.Equal(0, _sut.WeightedAverage([("entfällt", 1.0), ("krank", 1.0)]));
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedAverage_GewichtssummeNull_GibtNullZurueckStattDivisionDurchNull()
|
|
{
|
|
var result = _sut.WeightedAverage([("2", 0.0), ("4", 0.0)]);
|
|
Assert.Equal(0, result);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("3+", 2.6667)]
|
|
[InlineData("2-", 2.3333)]
|
|
[InlineData("1+", 0.6667)]
|
|
[InlineData("6-", 6.3333)]
|
|
public void WeightedAverage_NotenMitTendenz_VerschiebtUmEinDrittel(string grade, double expected)
|
|
{
|
|
var result = _sut.WeightedAverage([(grade, 1.0)]);
|
|
Assert.Equal(expected, result, precision: 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedAverage_NotenMitTendenzGemischtMitGanzenNoten_GewichtetKorrekt()
|
|
{
|
|
// (3+ [2,667] + 2 [2,0]) / 2 = 2,333
|
|
var result = _sut.WeightedAverage([("3+", 1.0), ("2", 1.0)]);
|
|
Assert.Equal(2.3333, result, precision: 4);
|
|
}
|
|
|
|
// ── RoundToGrade ──────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void RoundToGrade_Kaufmaennisch_RundetHalbeImmerVomNullpunktWeg()
|
|
{
|
|
Assert.Equal("3", _sut.RoundToGrade(2.5, GradingSystem.Grades1To6, RoundingRule.Commercial));
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundToGrade_Paedagogisch_RundetBeiNotenHalbeZurBesserenNote()
|
|
{
|
|
// Grades1To6: kleinere Zahl ist besser -> bei genau 2.5 wird abgerundet auf 2
|
|
Assert.Equal("2", _sut.RoundToGrade(2.5, GradingSystem.Grades1To6, RoundingRule.Pedagogical));
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundToGrade_Paedagogisch_RundetBeiPunktenHalbeZurBesserenNote()
|
|
{
|
|
// Points0To15: größere Zahl ist besser -> bei genau 10.5 wird aufgerundet auf 11
|
|
Assert.Equal("11", _sut.RoundToGrade(10.5, GradingSystem.Points0To15, RoundingRule.Pedagogical));
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundToGrade_Paedagogisch_OhneGenauenHalbenWert_VerhaeltSichWieKaufmaennisch()
|
|
{
|
|
Assert.Equal("2", _sut.RoundToGrade(2.3, GradingSystem.Grades1To6, RoundingRule.Pedagogical));
|
|
Assert.Equal("3", _sut.RoundToGrade(2.7, GradingSystem.Grades1To6, RoundingRule.Pedagogical));
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundToGrade_WertUnterhalbDesBereichs_WirdAufMinimumBegrenzt()
|
|
{
|
|
Assert.Equal("1", _sut.RoundToGrade(-3, GradingSystem.Grades1To6, RoundingRule.Commercial));
|
|
Assert.Equal("0", _sut.RoundToGrade(-3, GradingSystem.Points0To15, RoundingRule.Commercial));
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundToGrade_WertOberhalbDesBereichs_WirdAufMaximumBegrenzt()
|
|
{
|
|
Assert.Equal("6", _sut.RoundToGrade(9, GradingSystem.Grades1To6, RoundingRule.Commercial));
|
|
Assert.Equal("15", _sut.RoundToGrade(20, GradingSystem.Points0To15, RoundingRule.Commercial));
|
|
}
|
|
|
|
// ── ParticipationGrade ────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ParticipationGrade_MinimaleBewertung_GibtSchlechtesteNote()
|
|
{
|
|
Assert.Equal("6", _sut.ParticipationGrade(-2, GradingSystem.Grades1To6));
|
|
Assert.Equal("0", _sut.ParticipationGrade(-2, GradingSystem.Points0To15));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParticipationGrade_MaximaleBewertung_GibtBesteNote()
|
|
{
|
|
Assert.Equal("1", _sut.ParticipationGrade(2, GradingSystem.Grades1To6));
|
|
Assert.Equal("15", _sut.ParticipationGrade(2, GradingSystem.Points0To15));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParticipationGrade_MittlereBewertung_GibtMittlereNote()
|
|
{
|
|
Assert.Equal("4", _sut.ParticipationGrade(0, GradingSystem.Grades1To6));
|
|
}
|
|
|
|
// ── ValidateGradingScheme ────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ValidateGradingScheme_SummeHundert_IstGueltig()
|
|
{
|
|
var scheme = new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
|
|
Assert.Null(_sut.ValidateGradingScheme(scheme));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateGradingScheme_SummeUngleichHundert_LiefertFehler()
|
|
{
|
|
var scheme = new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 20 };
|
|
Assert.NotNull(_sut.ValidateGradingScheme(scheme));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateGradingScheme_NegativerAnteil_LiefertFehler()
|
|
{
|
|
var scheme = new GradingScheme { ExamsPercent = 110, ParticipationPercent = 0, OtherPercent = -10 };
|
|
Assert.NotNull(_sut.ValidateGradingScheme(scheme));
|
|
}
|
|
|
|
// ── CalculateReportGrade ──────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void CalculateReportGradeValue_LiefertUngerundetenGewichtetenWert()
|
|
{
|
|
var scheme = new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
|
|
|
|
var value = _sut.CalculateReportGradeValue([("2", 1.0)], [("3", 1.0)], [("1", 1.0)], scheme);
|
|
|
|
Assert.Equal(2.3, value!.Value, 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateReportGradeValue_OhneWerte_Null()
|
|
{
|
|
var scheme = new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
|
|
|
|
Assert.Null(_sut.CalculateReportGradeValue([], [], [], scheme));
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateReportGrade_AlleBereicheVorhanden_GewichtetGemaessSchema()
|
|
{
|
|
var scheme = new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
|
|
// Klausuren Ø2, Mitarbeit Ø3, Sonstige Ø1 -> (2*50 + 3*40 + 1*10) / 100 = 2.3 -> kaufmännisch 2
|
|
var result = _sut.CalculateReportGrade(
|
|
examGrades: [("2", 1)],
|
|
participationGrades: [("3", 1)],
|
|
otherGrades: [("1", 1)],
|
|
scheme, GradingSystem.Grades1To6, RoundingRule.Commercial);
|
|
Assert.Equal("2", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateReportGrade_FehlenderBereich_NormiertVerbleibendeAnteile()
|
|
{
|
|
var scheme = new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
|
|
// Keine sonstigen Noten -> nur Klausuren (50) und Mitarbeit (40) zählen, neu normiert auf 90.
|
|
// (2*50 + 4*40) / 90 = 2.888... -> kaufmännisch 3
|
|
var result = _sut.CalculateReportGrade(
|
|
examGrades: [("2", 1)],
|
|
participationGrades: [("4", 1)],
|
|
otherGrades: [],
|
|
scheme, GradingSystem.Grades1To6, RoundingRule.Commercial);
|
|
Assert.Equal("3", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateReportGrade_KeineWerteInIrgendeinemBereich_GibtNullZurueck()
|
|
{
|
|
var scheme = new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
|
|
var result = _sut.CalculateReportGrade([], [], [], scheme, GradingSystem.Grades1To6, RoundingRule.Commercial);
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateReportGrade_RundungsregelWirdBeruecksichtigt()
|
|
{
|
|
var scheme = new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 };
|
|
// Klausuren-Durchschnitt exakt 2.5 -> kaufmännisch 3, pädagogisch 2
|
|
var kaufmaennisch = _sut.CalculateReportGrade([("2", 1), ("3", 1)], [], [], scheme, GradingSystem.Grades1To6, RoundingRule.Commercial);
|
|
var paedagogisch = _sut.CalculateReportGrade([("2", 1), ("3", 1)], [], [], scheme, GradingSystem.Grades1To6, RoundingRule.Pedagogical);
|
|
Assert.Equal("3", kaufmaennisch);
|
|
Assert.Equal("2", paedagogisch);
|
|
}
|
|
}
|