Testabdeckung Kapitel 13.1 (Tests, Fehlerbehandlung)
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.
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user