Files
LehrerApp/LehrerApp.Desktop.Tests/ParticipationGradeAggregationTests.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

161 lines
6.1 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 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);
}
}