Files
LehrerApp/LehrerApp.Desktop.Tests/ReportGradeCalculationTests.cs
T
admin 273f459119 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.
2026-08-13 11:05:06 +02:00

198 lines
8.9 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)
{
var students = new FakeStudents([Anna, Ben]);
var memberships = new FakeMemberships([]);
var grading = new GradingService();
return new ReportGradeDialogViewModel(grades, exams, results, students, memberships,
schemes, reportGrades, grading, GroupId, GroupType.Class, GradingSystem.Grades1To6, "Testgruppe");
}
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 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 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);
}
}