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.
161 lines
6.1 KiB
C#
161 lines
6.1 KiB
C#
using LehrerApp.Core.Models;
|
||
using LehrerApp.Core.Services;
|
||
using LehrerApp.Desktop.ViewModels.Groups;
|
||
using Xunit;
|
||
|
||
namespace LehrerApp.Desktop.Tests;
|
||
|
||
/// Tests für die Mitarbeits-Aggregation (3.2): ParticipationGradeDialogViewModel
|
||
/// verrechnet die Sitzungsbewertungen eines Zeitraums zu einer Mitarbeitsnote je Schüler.
|
||
public class ParticipationGradeAggregationTests
|
||
{
|
||
private static (ParticipationGradeDialogViewModel Vm, Guid AnnaId, Guid BenId, Guid CoraId, Guid GroupId, FakeGrades Grades)
|
||
BuildScenario()
|
||
{
|
||
var groupId = Guid.NewGuid();
|
||
var anna = new Student { Id = Guid.NewGuid(), FirstName = "Anna", LastName = "Beispiel" };
|
||
var ben = new Student { Id = Guid.NewGuid(), FirstName = "Ben", LastName = "Muster" };
|
||
var cora = new Student { Id = Guid.NewGuid(), FirstName = "Cora", LastName = "Ohne" }; // nie bewertet
|
||
|
||
var students = new FakeStudents([anna, ben, cora]);
|
||
var memberships = new FakeMemberships([]);
|
||
var aspects = new FakeAspects();
|
||
|
||
var sess1 = new ParticipationSession { GroupId = groupId, Date = new DateOnly(2025, 9, 1) };
|
||
var sess2 = new ParticipationSession { GroupId = groupId, Date = new DateOnly(2025, 9, 15) };
|
||
var sess3 = new ParticipationSession { GroupId = groupId, Date = new DateOnly(2025, 9, 29) };
|
||
var sessions = new FakeSessions([sess1, sess2, sess3]);
|
||
|
||
var entries = new FakeEntries();
|
||
// Anna: −1, 0, +2 → steigender Trend, beide Standardaspekte gleich bewertet.
|
||
entries.Add(new ParticipationEntry
|
||
{
|
||
SessionId = sess1.Id, StudentId = anna.Id,
|
||
Ratings = [new() { Key = "quality", Value = -1 }, new() { Key = "quantity", Value = -1 }],
|
||
});
|
||
entries.Add(new ParticipationEntry
|
||
{
|
||
SessionId = sess2.Id, StudentId = anna.Id,
|
||
Ratings = [new() { Key = "quality", Value = 0 }, new() { Key = "quantity", Value = 0 }],
|
||
});
|
||
entries.Add(new ParticipationEntry
|
||
{
|
||
SessionId = sess3.Id, StudentId = anna.Id,
|
||
Ratings = [new() { Key = "quality", Value = 2 }, new() { Key = "quantity", Value = 2 }],
|
||
});
|
||
// Ben: nur einmal bewertet.
|
||
entries.Add(new ParticipationEntry
|
||
{
|
||
SessionId = sess1.Id, StudentId = ben.Id,
|
||
Ratings = [new() { Key = "quality", Value = 0 }],
|
||
});
|
||
// Cora: nie bewertet.
|
||
|
||
var grades = new FakeGrades();
|
||
var grading = new GradingService();
|
||
|
||
var vm = new ParticipationGradeDialogViewModel(sessions, entries, aspects, students, memberships,
|
||
grades, grading, groupId, "2025/26", GradingSystem.Grades1To6);
|
||
|
||
return (vm, anna.Id, ben.Id, cora.Id, groupId, grades);
|
||
}
|
||
|
||
[Fact]
|
||
public void Recompute_MittelBerechnungGewichtetAlleSitzungenGleich()
|
||
{
|
||
var (vm, annaId, _, _, _, _) = BuildScenario();
|
||
|
||
var anna = vm.Rows.Single(r => r.StudentId == annaId);
|
||
|
||
Assert.Equal(3, anna.SessionCount);
|
||
Assert.Equal((-1.0 + 0.0 + 2.0) / 3.0, double.Parse(anna.AverageDisplay.Replace(',', '.'),
|
||
System.Globalization.CultureInfo.InvariantCulture), precision: 2);
|
||
}
|
||
|
||
[Fact]
|
||
public void Recompute_NichtBewerteterSchueler_HatKeineNoteStattSchlechterNote()
|
||
{
|
||
var (vm, _, _, coraId, _, _) = BuildScenario();
|
||
|
||
var cora = vm.Rows.Single(r => r.StudentId == coraId);
|
||
|
||
Assert.Equal(0, cora.SessionCount);
|
||
Assert.Null(cora.Grade);
|
||
Assert.Equal("nicht bewertet", cora.GradeDisplay);
|
||
}
|
||
|
||
[Fact]
|
||
public void Recompute_EinzelneBewertung_ErgibtTrotzdemEineNote()
|
||
{
|
||
var (vm, _, benId, _, _, _) = BuildScenario();
|
||
|
||
var ben = vm.Rows.Single(r => r.StudentId == benId);
|
||
|
||
Assert.Equal(1, ben.SessionCount);
|
||
Assert.NotNull(ben.Grade);
|
||
}
|
||
|
||
[Fact]
|
||
public void Recompute_SteigendeBewertungen_ZeigenAufwaertstrend()
|
||
{
|
||
var (vm, annaId, _, _, _, _) = BuildScenario();
|
||
|
||
var anna = vm.Rows.Single(r => r.StudentId == annaId);
|
||
|
||
Assert.Equal("↑", anna.TrendSymbol);
|
||
}
|
||
|
||
[Fact]
|
||
public void AspektGewicht_AufNullGesetzt_NimmtAspektAusDerBerechnungHeraus()
|
||
{
|
||
var (vm, annaId, _, _, _, _) = BuildScenario();
|
||
var quantity = vm.AspectWeights.First(a => a.Key == "quantity");
|
||
|
||
quantity.Weight = 0;
|
||
|
||
// Nur noch "quality" zählt: (-1 + 0 + 2) / 3 = 0.33 — identisch zum Gesamtmittel,
|
||
// da beide Aspekte bei Anna durchgehend gleich bewertet wurden.
|
||
var anna = vm.Rows.Single(r => r.StudentId == annaId);
|
||
var average = double.Parse(anna.AverageDisplay.Replace(',', '.'), System.Globalization.CultureInfo.InvariantCulture);
|
||
Assert.Equal(1.0 / 3.0, average, precision: 2);
|
||
}
|
||
|
||
[Fact]
|
||
public void SelectedPeriod_H1_SchliesstSitzungenAusserhalbAus()
|
||
{
|
||
var (vm, _, _, _, _, _) = BuildScenario();
|
||
|
||
vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.H2);
|
||
|
||
// Alle Testsitzungen liegen im September (H1) -> im H2-Zeitraum bleibt niemand bewertet.
|
||
Assert.All(vm.Rows, r => Assert.Equal(0, r.SessionCount));
|
||
}
|
||
|
||
[Fact]
|
||
public void Apply_SpeichertEineNoteJeBewertetemSchueler()
|
||
{
|
||
var (vm, annaId, benId, coraId, groupId, grades) = BuildScenario();
|
||
|
||
vm.ApplyCommand.Execute(null);
|
||
|
||
Assert.NotNull(grades.GetByStudentAndGroup(annaId, groupId)
|
||
.SingleOrDefault(g => g.Category == GradeCategory.Participation));
|
||
Assert.NotNull(grades.GetByStudentAndGroup(benId, groupId)
|
||
.SingleOrDefault(g => g.Category == GradeCategory.Participation));
|
||
Assert.Empty(grades.GetByStudentAndGroup(coraId, groupId));
|
||
}
|
||
|
||
[Fact]
|
||
public void Apply_ZweitesMal_AktualisiertBestehendeNoteStattZuDuplizieren()
|
||
{
|
||
var (vm, annaId, _, _, groupId, grades) = BuildScenario();
|
||
|
||
vm.ApplyCommand.Execute(null);
|
||
vm.ApplyCommand.Execute(null);
|
||
|
||
var annaGrades = grades.GetByStudentAndGroup(annaId, groupId)
|
||
.Where(g => g.Category == GradeCategory.Participation).ToList();
|
||
Assert.Single(annaGrades);
|
||
}
|
||
}
|