Files
LehrerApp/LehrerApp.Desktop.Tests/ReportGradeCalculationTests.cs
T
adminandClaude Opus 5.5 67c5a906d1
CI / build-and-test (push) Canceled after 0s
Zeugnisnoten-Assistent mit Mitarbeitsverlauf und Prüfungsnoten Klasse 10
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>
2026-10-06 12:09:33 +02:00

249 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
namespace LehrerApp.Desktop.Tests;
/// Tests für die Zeugnisnotenberechnung (2.4): ReportGradeDialogViewModel kombiniert
/// Klausuren, Mitarbeit und sonstige Noten gemäß Gewichtungsschema zu einer Zeugnisnote.
public class ReportGradeCalculationTests
{
private static Guid GroupId { get; } = Guid.NewGuid();
private static Student Anna { get; } = new() { Id = Guid.NewGuid(), FirstName = "Anna", LastName = "Beispiel" };
private static Student Ben { get; } = new() { Id = Guid.NewGuid(), FirstName = "Ben", LastName = "Muster" };
private static ReportGradeDialogViewModel BuildViewModel(
FakeGrades grades, FakeSchemes schemes, FakeReportGrades reportGrades,
FakeExams exams, FakeResults results, FakeSessions? sessions = null, FakeEntries? entries = null)
{
var students = new FakeStudents([Anna, Ben]);
var memberships = new FakeMemberships([]);
var grading = new GradingService();
return new ReportGradeDialogViewModel(grades, exams, results, students, memberships,
schemes, reportGrades, sessions ?? new FakeSessions([]), entries ?? new FakeEntries(),
new AttendanceBalanceService(), grading, GroupId, GroupType.Class, GradingSystem.Grades1To6,
"Testgruppe", "2025/26");
}
private static (Exam Exam1, Exam Exam2, FakeExams Exams, FakeResults Results) BuildExams()
{
var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" };
var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 24), Title = "K2" };
var exams = new FakeExams([exam1, exam2]);
var results = new FakeResults();
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "2" });
results.Add(new ExamResult { ExamId = exam2.Id, StudentId = Anna.Id, Grade = "4" });
return (exam1, exam2, exams, results);
}
[Fact]
public void CalculatedGrade_KombiniertKlausurenMitarbeitUndSonstigeGemaessSchema()
{
var (_, _, exams, results) = BuildExams();
var grades = new FakeGrades();
grades.Add(new Grade { StudentId = Anna.Id, GroupId = GroupId, Category = GradeCategory.Participation,
Date = new DateOnly(2025, 9, 5), Value = "3", Weight = 1.0 });
grades.Add(new Grade { StudentId = Anna.Id, GroupId = GroupId, Category = GradeCategory.Other,
Date = new DateOnly(2025, 9, 5), Value = "1", Weight = 1.0 });
var schemes = new FakeSchemes();
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 });
var vm = BuildViewModel(grades, schemes, new FakeReportGrades(), exams, results);
// Klausuren Ø3 (Note 2 und 4), Mitarbeit 3, Sonstige 1 -> (3*50+3*40+1*10)/100 = 2.8 -> kaufmännisch 3
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
Assert.Equal("3", anna.CalculatedValue);
}
[Fact]
public void CalculatedGrade_SchuelerOhneJedeNote_ZeigtKeinenWert()
{
var (_, _, exams, results) = BuildExams();
var schemes = new FakeSchemes();
var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results);
var ben = vm.Rows.Single(r => r.StudentId == Ben.Id);
Assert.Null(ben.CalculatedValue);
Assert.Equal("–", ben.CalculatedDisplay);
}
[Fact]
public void AbsenceRatePercent_BerechnetFehlquoteAusSitzungenDieserGruppeImZeitraum()
{
var (_, _, exams, results) = BuildExams();
var session1 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 5) };
var session2 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 12) };
var sessions = new FakeSessions([session1, session2]);
var entries = new FakeEntries();
entries.Add(new ParticipationEntry { SessionId = session1.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Present });
entries.Add(new ParticipationEntry { SessionId = session2.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Unexcused });
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results,
sessions, entries);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
Assert.Equal(50.0, anna.AbsenceRatePercent);
Assert.True(anna.HasHighAbsenceRate);
var ben = vm.Rows.Single(r => r.StudentId == Ben.Id);
Assert.Equal(0.0, ben.AbsenceRatePercent);
Assert.False(ben.HasHighAbsenceRate);
}
[Fact]
public void ResolveScheme_BevorzugtGruppenspezifischesSchemaVorVoreinstellung()
{
var (_, _, exams, results) = BuildExams();
var grades = new FakeGrades();
var schemes = new FakeSchemes();
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 });
schemes.SetDefault(GroupType.Class, new GradingScheme { ExamsPercent = 0, ParticipationPercent = 0, OtherPercent = 100 });
var vm = BuildViewModel(grades, schemes, new FakeReportGrades(), exams, results);
// Gruppenspezifisches Schema gewichtet nur Klausuren (Ø3 aus Note 2 und 4) -> exakt "3"
Assert.Contains("Klausuren 100", vm.SchemeSummary);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
Assert.Equal("3", anna.CalculatedValue);
}
[Fact]
public void ResolveScheme_OhneGruppenschema_VerwendetVoreinstellungDesGruppentyps()
{
var (_, _, exams, results) = BuildExams();
var schemes = new FakeSchemes();
schemes.SetDefault(GroupType.Class, new GradingScheme { ExamsPercent = 70, ParticipationPercent = 20, OtherPercent = 10 });
var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results);
Assert.Contains("Klausuren 70", vm.SchemeSummary);
}
[Fact]
public void ResolveScheme_OhneJedeVoreinstellung_VerwendetHartcodiertenFallback()
{
var (_, _, exams, results) = BuildExams();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results);
Assert.Contains("Klausuren 50", vm.SchemeSummary);
Assert.Contains("Mitarbeit 40", vm.SchemeSummary);
Assert.Contains("Sonstige 10", vm.SchemeSummary);
}
[Fact]
public void Save_UebersteuernOhneBegruendung_LiefertValidierungsfehlerUndSpeichertNicht()
{
var (_, _, exams, results) = BuildExams();
var reportGrades = new FakeReportGrades();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
anna.OverrideValue = "1";
anna.SaveCommand.Execute(null);
Assert.False(string.IsNullOrEmpty(anna.ValidationMessage));
Assert.Equal(0, reportGrades.SavedCount);
}
[Fact]
public void Save_UebersteuernMitBegruendung_AktualisiertFinalDisplay()
{
var (_, _, exams, results) = BuildExams();
var reportGrades = new FakeReportGrades();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
anna.OverrideValue = "1";
anna.OverrideReason = "Mündliche Steigerung im letzten Quartal";
anna.SaveCommand.Execute(null);
Assert.True(string.IsNullOrEmpty(anna.ValidationMessage));
Assert.Equal("1", anna.FinalDisplay);
Assert.Equal(1, reportGrades.SavedCount);
}
[Fact]
public void ToggleLock_FriertDenStandEinUndIgnoriertSpaetereNeuberechnung()
{
var (exam1, _, exams, results) = BuildExams();
var reportGrades = new FakeReportGrades();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
anna.OverrideValue = "1";
anna.OverrideReason = "Testgrund";
anna.SaveCommand.Execute(null);
anna.ToggleLockCommand.Execute(null);
// Neue, bessere Klausurnote nachtragen -> würde die Berechnung ändern, wenn ungesperrt.
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "1" });
vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.H1);
vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.FullYear);
var annaNachSperre = vm.Rows.Single(r => r.StudentId == Anna.Id);
Assert.True(annaNachSperre.IsLocked);
Assert.Equal("1", annaNachSperre.FinalDisplay);
}
[Fact]
public void Schuljahr_WirdAusHalbjahresnotenUndPruefungsnotenVerrechnet()
{
// H1: eine 1 und eine 2 -> 1,5 -> 2. H2: eine 4 -> 4.
var h1a = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" };
var h1b = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 11, 10), Title = "K2" };
var h2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2026, 3, 10), Title = "K3" };
var results = new FakeResults();
results.Add(new ExamResult { ExamId = h1a.Id, StudentId = Anna.Id, Grade = "1" });
results.Add(new ExamResult { ExamId = h1b.Id, StudentId = Anna.Id, Grade = "2" });
results.Add(new ExamResult { ExamId = h2.Id, StudentId = Anna.Id, Grade = "4" });
var reportGrades = new FakeReportGrades();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, new FakeExams([h1a, h1b, h2]), results);
// Aus den Halbjahresnoten: (2 + 4) / 2 = 3 — nicht aus allen Arbeiten ((1+2+4)/3 = 2,33 -> 2).
Assert.Equal("3", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
// Klasse 10: eine im Fach abgelegte Projektprüfung zählt wie ein drittes Halbjahr.
reportGrades.Save(new ReportGrade { StudentId = Anna.Id, GroupId = GroupId, Period = "Gesamtes Schuljahr",
ProjectExamGrade = "6" });
vm.Refresh();
// (2 + 4 + 6) / 3 = 4
Assert.Equal("4", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
}
[Fact]
public void ExportCsv_EnthaeltAlleSchuelerDerGruppe()
{
var (_, _, exams, results) = BuildExams();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results);
var csv = vm.ExportCsv();
Assert.Contains("Beispiel, Anna", csv);
Assert.Contains("Muster, Ben", csv);
}
[Fact]
public void RoundingRule_Paedagogisch_RundetGrenzfallZurBesserenNote()
{
var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" };
var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 24), Title = "K2" };
var exams = new FakeExams([exam1, exam2]);
var results = new FakeResults();
// Ø exakt 2.5 -> kaufmännisch 3, pädagogisch 2 (bessere Note)
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "2" });
results.Add(new ExamResult { ExamId = exam2.Id, StudentId = Anna.Id, Grade = "3" });
var schemes = new FakeSchemes();
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 });
var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results);
Assert.Equal("3", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
vm.RoundingRuleName = "Pädagogisch";
Assert.Equal("2", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
}
}