Files
LehrerApp/LehrerApp.Desktop.Tests/ParticipationGradeAggregationTests.cs
T
adminandClaude Sonnet 5 16ff4875d8 3.1.3: Aspekt-Typen Scale3/Binary/Points in Raster und Dialog unterstützt
Neue Klasse ParticipationRatingScale (Core) ist die einzige Quelle für
Rohwerte je Aspekt-Typ. Scale3/Binary liegen bewusst direkt auf derselben
-2..+2-Achse wie Scale5 (nur mit weniger Zwischenschritten), damit die
bestehende Gewichtung/Mittelwertbildung zur Mitarbeitsnote unverändert
kompatibel bleibt. Points ist grundverschieden (echter Zählwert 0..MaxPoints,
neues Feld auf ParticipationAspect) und wird nur zur Aggregation linear auf
dieselbe Achse normiert.

Ohne diese Normierung hätte ein Punkte-Aspekt die Mitarbeitsnote verfälscht:
drei Stellen summierten bisher den Rohwert direkt (ParticipationGradeDialog-
ViewModel.Recompute, ParticipationWizardViewModels.WeightedRating und
.ComputeSuggestion) - alle drei sind jetzt auf die Normierung umgestellt.

Raster: Punkte-Aspekte zeigen ein NumericUpDown statt fester Stufen-Buttons.
Schnelleingabe-Dialog: Zifferntasten/+/- sind jetzt typabhängig, Legende
zeigt live die für den aktuellen Aspekt gültigen Tasten. Aspekt-Verwaltung
um "Max. Punkte"-Feld ergänzt (nur bei Typ "Punkte" sichtbar).

Bewusst nicht angefasst: die Trendlinien-Visualisierung im
Mitarbeits-Assistenten bleibt fest auf die drei Standardaspekte
zugeschnitten - eigener, größerer Umbau.

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

200 lines
7.8 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);
}
/// 3.1.3: ein Punkte-Aspekt darf nicht als roher Zahlenwert in die Mittelwertbildung
/// einfließen (z.B. "4 Punkte" wäre auf der -2..+2-Achse ein Extremwert weit über +2) —
/// er muss vorher auf dieselbe Achse normiert werden wie Scale5/Scale3/Binary.
[Fact]
public void Recompute_PunkteAspekt_WirdVorDerMittelungAufMinus2Bis2AchseNormiert()
{
var groupId = Guid.NewGuid();
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
var students = new FakeStudents([anna]);
var memberships = new FakeMemberships([]);
var aspects = new FakeAspects();
aspects.Save(new ParticipationAspect
{
GroupId = groupId, Key = "raised_hand", Label = "Meldungen",
ValueType = AspectValueType.Points, MaxPoints = 4,
});
var session = new ParticipationSession { GroupId = groupId, Date = new DateOnly(2025, 9, 1) };
var sessions = new FakeSessions([session]);
var entries = new FakeEntries();
// 2 von 4 Punkten -> Mitte der Skala, normiert genau auf 0.
entries.Add(new ParticipationEntry
{
SessionId = session.Id, StudentId = anna.Id,
Ratings = [new() { Key = "raised_hand", Value = 2 }],
});
var grades = new FakeGrades();
var grading = new GradingService();
var vm = new ParticipationGradeDialogViewModel(sessions, entries, aspects, students, memberships,
grades, grading, groupId, "2025/26", GradingSystem.Grades1To6);
var row = Assert.Single(vm.Rows);
Assert.Equal(0.0, double.Parse(row.AverageDisplay.Replace(',', '.'),
System.Globalization.CultureInfo.InvariantCulture), precision: 2);
}
}