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.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
// Einfache In-Memory-Fakes der Repository-Schnittstellen, damit ViewModel-Tests ohne echte
|
||||
// LiteDB-Anbindung laufen. Bewusst schlank gehalten: nur was die getesteten ViewModels brauchen.
|
||||
|
||||
public class FakeStudents(List<Student> all) : IStudentRepository
|
||||
{
|
||||
public Student? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id);
|
||||
public List<Student> GetAll(bool includeInactive = false) => all;
|
||||
public List<Student> GetByGroup(Guid groupId) => all;
|
||||
public void Save(Student student) { }
|
||||
public void Delete(Guid id) { }
|
||||
}
|
||||
|
||||
public class FakeMemberships(List<GroupMembership> all) : IGroupMembershipRepository
|
||||
{
|
||||
public List<GroupMembership> GetByStudent(Guid studentId) => all.Where(m => m.StudentId == studentId).ToList();
|
||||
public List<GroupMembership> GetByGroup(Guid groupId) => all.Where(m => m.GroupId == groupId).ToList();
|
||||
public GroupMembership? GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
||||
all.FirstOrDefault(m => m.StudentId == studentId && m.GroupId == groupId);
|
||||
public void Save(GroupMembership membership) { }
|
||||
public void Delete(Guid id) { }
|
||||
}
|
||||
|
||||
public class FakeSessions(List<ParticipationSession> all) : IParticipationSessionRepository
|
||||
{
|
||||
public List<ParticipationSession> GetByGroup(Guid groupId) => all.Where(s => s.GroupId == groupId).ToList();
|
||||
public ParticipationSession? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id);
|
||||
public void Save(ParticipationSession session) { }
|
||||
public void Delete(Guid id) { }
|
||||
}
|
||||
|
||||
public class FakeEntries : IParticipationRepository
|
||||
{
|
||||
private readonly List<ParticipationEntry> _all = [];
|
||||
public void Add(ParticipationEntry e) => _all.Add(e);
|
||||
public List<ParticipationEntry> GetBySession(Guid sessionId) => _all.Where(e => e.SessionId == sessionId).ToList();
|
||||
public List<ParticipationEntry> GetByStudent(Guid studentId) => _all.Where(e => e.StudentId == studentId).ToList();
|
||||
public ParticipationEntry? GetBySessionAndStudent(Guid sessionId, Guid studentId) =>
|
||||
_all.FirstOrDefault(e => e.SessionId == sessionId && e.StudentId == studentId);
|
||||
public void Save(ParticipationEntry entry)
|
||||
{
|
||||
_all.RemoveAll(e => e.Id == entry.Id);
|
||||
_all.Add(entry);
|
||||
}
|
||||
public void SaveMany(List<ParticipationEntry> entries) { foreach (var e in entries) Save(e); }
|
||||
public void DeleteBySession(Guid sessionId) => _all.RemoveAll(e => e.SessionId == sessionId);
|
||||
}
|
||||
|
||||
public class FakeAspects : IParticipationAspectRepository
|
||||
{
|
||||
public List<ParticipationAspect> GetDefaults() => [];
|
||||
public List<ParticipationAspect> GetByGroup(Guid groupId) => [];
|
||||
public void Save(ParticipationAspect aspect) { }
|
||||
public void Delete(Guid id) { }
|
||||
}
|
||||
|
||||
public class FakeGrades : IGradeRepository
|
||||
{
|
||||
private readonly List<Grade> _all = [];
|
||||
public void Add(Grade g) => _all.Add(g);
|
||||
public List<Grade> GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
||||
_all.Where(g => g.StudentId == studentId && g.GroupId == groupId).ToList();
|
||||
public List<Grade> GetByGroup(Guid groupId) => _all.Where(g => g.GroupId == groupId).ToList();
|
||||
public void Save(Grade grade)
|
||||
{
|
||||
_all.RemoveAll(g => g.Id == grade.Id);
|
||||
_all.Add(grade);
|
||||
}
|
||||
public void Delete(Guid id) => _all.RemoveAll(g => g.Id == id);
|
||||
}
|
||||
|
||||
public class FakeExams(List<Exam> all) : IExamRepository
|
||||
{
|
||||
public Exam? GetById(Guid id) => all.FirstOrDefault(e => e.Id == id);
|
||||
public List<Exam> GetByGroup(Guid groupId) => all.Where(e => e.GroupId == groupId).ToList();
|
||||
public void Save(Exam exam) { }
|
||||
public void Delete(Guid id) { }
|
||||
}
|
||||
|
||||
public class FakeResults : IExamResultRepository
|
||||
{
|
||||
private readonly List<ExamResult> _all = [];
|
||||
// Upsert-Semantik nach ExamId+StudentId, wie der reale unique Index es erzwingt.
|
||||
public void Add(ExamResult r)
|
||||
{
|
||||
_all.RemoveAll(x => x.ExamId == r.ExamId && x.StudentId == r.StudentId);
|
||||
_all.Add(r);
|
||||
}
|
||||
public List<ExamResult> GetByExam(Guid examId) => _all.Where(r => r.ExamId == examId).ToList();
|
||||
public List<ExamResult> GetByStudent(Guid studentId) => _all.Where(r => r.StudentId == studentId).ToList();
|
||||
public ExamResult? GetByExamAndStudent(Guid examId, Guid studentId) =>
|
||||
_all.FirstOrDefault(r => r.ExamId == examId && r.StudentId == studentId);
|
||||
public void Save(ExamResult result) { }
|
||||
public void SaveMany(List<ExamResult> results) { }
|
||||
}
|
||||
|
||||
public class FakeSchemes : IGradingSchemeRepository
|
||||
{
|
||||
private readonly Dictionary<Guid, GradingScheme> _byGroup = [];
|
||||
private readonly Dictionary<GroupType, GradingScheme> _byType = [];
|
||||
public void SetForGroup(Guid groupId, GradingScheme s) => _byGroup[groupId] = s;
|
||||
public void SetDefault(GroupType type, GradingScheme s) => _byType[type] = s;
|
||||
public GradingScheme? GetByGroup(Guid groupId) => _byGroup.GetValueOrDefault(groupId);
|
||||
public GradingScheme? GetDefaultForType(GroupType type) => _byType.GetValueOrDefault(type);
|
||||
public void Save(GradingScheme scheme) { }
|
||||
public void Delete(Guid id) { }
|
||||
}
|
||||
|
||||
public class FakeReportGrades : IReportGradeRepository
|
||||
{
|
||||
private readonly List<ReportGrade> _all = [];
|
||||
public int SavedCount { get; private set; }
|
||||
public List<ReportGrade> GetByGroup(Guid groupId) => _all.Where(r => r.GroupId == groupId).ToList();
|
||||
public ReportGrade? GetByStudentGroupPeriod(Guid studentId, Guid groupId, string period) =>
|
||||
_all.FirstOrDefault(r => r.StudentId == studentId && r.GroupId == groupId && r.Period == period);
|
||||
public void Save(ReportGrade grade)
|
||||
{
|
||||
_all.RemoveAll(r => r.Id == grade.Id);
|
||||
_all.Add(grade);
|
||||
SavedCount++;
|
||||
}
|
||||
public void Delete(Guid id) => _all.RemoveAll(r => r.Id == id);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LehrerApp.Desktop\LehrerApp.Desktop.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,160 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user