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); } // ── 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 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); } }