Vier neue xUnit-Testprojekte (LehrerApp.Tests, .Data.Tests, .Desktop.Tests, .Sync.Tests) mit zusammen 104 Tests: GradingService, SchoolYearService, Repositories gegen In-Memory-LiteDB, Mitarbeits-Aggregation (3.2), Zeugnisnotenberechnung (2.4) und ConflictResolver. Dabei zwei echte Fehler in der Sync-Schicht gefunden und behoben: - SyncEvent.EventId fehlte [BsonId], wodurch EventQueue.Acknowledge() nie etwas aus der Queue löschte. - ConflictResolver verglich Zeitstempel unterschiedlicher DateTimeKind direkt (LiteDB liefert Local statt Utc zurück), was die Gleichstand- Regel außerhalb von UTC+0 verfälschte. SchoolYearService.CurrentSchoolYear()/RecentSchoolYears() um ein optionales today-Argument erweitert, um den Schuljahreswechsel deterministisch zu testen; LiteDbContext um einen Stream-Konstruktor für In-Memory-Tests.
320 lines
12 KiB
C#
320 lines
12 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);
|
|
}
|
|
|
|
// ── 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);
|
|
}
|
|
}
|