Files
LehrerApp/LehrerApp.Tests/GradingServiceTests.cs
adminandClaude Sonnet 5 a088823b30
CI / build-and-test (push) Canceled after 0s
Notentendenzen (3+/2-) korrekt in Notenschnitt einrechnen, Abschnittsende-Vorschlag aus Einheit
Bisher liess int.TryParse jede Note mit Tendenz durchfallen und WeightedAverage rechnete den
betroffenen Bereich mit 0 statt sie auszulassen - faelschte Notenuebersicht, Zielnoten- und
Was-waere-wenn-Rechner. Ausserdem schlaegt der Mitarbeits-Assistent beim Abschnitt-Abschliessen
jetzt das Ende der zuletzt abgeschlossenen Unterrichtseinheit statt immer "heute" vor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 13:24:16 +02:00

339 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 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);
}
}