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:
2026-08-13 11:05:06 +02:00
parent a8b8a40b52
commit 273f459119
15 changed files with 1558 additions and 14 deletions
+334
View File
@@ -0,0 +1,334 @@
using LehrerApp.Core.Models;
using LehrerApp.Data.Repositories;
using Xunit;
namespace LehrerApp.Data.Tests;
public sealed class RepositoryTests
{
private static LiteDbContext NewInMemoryContext() => new(new MemoryStream());
// ── StudentRepository ─────────────────────────────────────────────────────
[Fact]
public void StudentRepository_GetByGroup_FindetNurZugeordneteSchueler()
{
using var db = NewInMemoryContext();
var repo = new StudentRepository(db);
var groupId = Guid.NewGuid();
var inGroup = new Student { FirstName = "Anna", LastName = "Beispiel" };
var notInGroup = new Student { FirstName = "Ben", LastName = "Muster" };
repo.Save(inGroup);
repo.Save(notInGroup);
db.Memberships.Insert(new GroupMembership { StudentId = inGroup.Id, GroupId = groupId });
var result = repo.GetByGroup(groupId);
Assert.Single(result);
Assert.Equal(inGroup.Id, result[0].Id);
}
[Fact]
public void StudentRepository_GetAll_OhneInactiveFiltertDeaktivierteSchuelerAus()
{
using var db = NewInMemoryContext();
var repo = new StudentRepository(db);
repo.Save(new Student { FirstName = "Anna", LastName = "Aktiv", IsActive = true });
repo.Save(new Student { FirstName = "Ben", LastName = "Inaktiv", IsActive = false });
Assert.Single(repo.GetAll(includeInactive: false));
Assert.Equal(2, repo.GetAll(includeInactive: true).Count);
}
// ── GroupRepository ───────────────────────────────────────────────────────
[Fact]
public void GroupRepository_Save_LehntUnbekanntesFachAb()
{
using var db = NewInMemoryContext();
var repo = new GroupRepository(db);
var group = new LearningGroup { Name = "Mathe G", SchoolYear = "2025/26", SubjectId = Guid.NewGuid() };
Assert.Throws<InvalidOperationException>(() => repo.Save(group));
}
[Fact]
public void GroupRepository_Save_AkzeptiertVorhandenesFach()
{
using var db = NewInMemoryContext();
var subjectRepo = new SubjectRepository(db);
var groupRepo = new GroupRepository(db);
var subject = new Subject { Name = "Mathematik" };
subjectRepo.Save(subject);
groupRepo.Save(new LearningGroup { Name = "Mathe G", SchoolYear = "2025/26", SubjectId = subject.Id });
Assert.NotNull(groupRepo.GetAll(includeInactive: true).FirstOrDefault(g => g.Name == "Mathe G"));
}
[Fact]
public void GroupRepository_Delete_LoeschtAlleAbhaengigenDatensaetzeKaskadierend()
{
using var db = NewInMemoryContext();
var groupRepo = new GroupRepository(db);
var groupId = Guid.NewGuid();
groupRepo.Save(new LearningGroup { Id = groupId, Name = "Mathe G", SchoolYear = "2025/26" });
var studentId = Guid.NewGuid();
var membershipId = Guid.NewGuid();
db.Memberships.Insert(new GroupMembership { Id = membershipId, StudentId = studentId, GroupId = groupId });
var examId = Guid.NewGuid();
var examResultId = Guid.NewGuid();
db.Exams.Insert(new Exam { Id = examId, GroupId = groupId });
db.ExamResults.Insert(new ExamResult { Id = examResultId, ExamId = examId, StudentId = studentId });
var gradeId = Guid.NewGuid();
db.Grades.Insert(new Grade { Id = gradeId, GroupId = groupId, StudentId = studentId });
var unitId = Guid.NewGuid();
var lessonInUnitId = Guid.NewGuid();
db.Units.Insert(new Unit { Id = unitId, GroupId = groupId });
db.Lessons.Insert(new Lesson { Id = lessonInUnitId, UnitId = unitId, GroupId = groupId });
var looseLessonId = Guid.NewGuid();
db.Lessons.Insert(new Lesson { Id = looseLessonId, UnitId = Guid.NewGuid(), GroupId = groupId });
var sessionId = Guid.NewGuid();
var entryId = Guid.NewGuid();
db.ParticipationSessions.Insert(new ParticipationSession { Id = sessionId, GroupId = groupId });
db.ParticipationEntries.Insert(new ParticipationEntry { Id = entryId, SessionId = sessionId, StudentId = studentId });
var aspectId = Guid.NewGuid();
db.ParticipationAspects.Insert(new ParticipationAspect { Id = aspectId, GroupId = groupId, Key = "quality" });
groupRepo.Delete(groupId);
Assert.Null(db.Groups.FindById(groupId));
Assert.Null(db.Memberships.FindById(membershipId));
Assert.Null(db.Exams.FindById(examId));
Assert.Null(db.ExamResults.FindById(examResultId));
Assert.Null(db.Grades.FindById(gradeId));
Assert.Null(db.Units.FindById(unitId));
Assert.Null(db.Lessons.FindById(lessonInUnitId));
Assert.Null(db.Lessons.FindById(looseLessonId));
Assert.Null(db.ParticipationSessions.FindById(sessionId));
Assert.Null(db.ParticipationEntries.FindById(entryId));
Assert.Null(db.ParticipationAspects.FindById(aspectId));
}
[Fact]
public void GroupRepository_Delete_LaesstDatenAndererGruppenUnberuehrt()
{
using var db = NewInMemoryContext();
var groupRepo = new GroupRepository(db);
var deletedGroupId = Guid.NewGuid();
var otherGroupId = Guid.NewGuid();
groupRepo.Save(new LearningGroup { Id = deletedGroupId, Name = "Löschen", SchoolYear = "2025/26" });
groupRepo.Save(new LearningGroup { Id = otherGroupId, Name = "Bleibt", SchoolYear = "2025/26" });
var otherExamId = Guid.NewGuid();
db.Exams.Insert(new Exam { Id = otherExamId, GroupId = otherGroupId });
groupRepo.Delete(deletedGroupId);
Assert.Null(db.Groups.FindById(deletedGroupId));
Assert.NotNull(db.Groups.FindById(otherGroupId));
Assert.NotNull(db.Exams.FindById(otherExamId));
}
// ── GroupMembershipRepository ────────────────────────────────────────────
[Fact]
public void GroupMembershipRepository_Save_LehntZweiteZuordnungFuerGleichesPaarAb()
{
using var db = NewInMemoryContext();
var repo = new GroupMembershipRepository(db);
var studentId = Guid.NewGuid();
var groupId = Guid.NewGuid();
repo.Save(new GroupMembership { StudentId = studentId, GroupId = groupId });
var duplicate = new GroupMembership { StudentId = studentId, GroupId = groupId };
Assert.Throws<InvalidOperationException>(() => repo.Save(duplicate));
}
[Fact]
public void GroupMembershipRepository_Save_ErlaubtAktualisierenDerselbenZuordnung()
{
using var db = NewInMemoryContext();
var repo = new GroupMembershipRepository(db);
var membership = new GroupMembership { StudentId = Guid.NewGuid(), GroupId = Guid.NewGuid(), Period = MembershipPeriod.FullYear };
repo.Save(membership);
membership.Period = MembershipPeriod.H1Only;
repo.Save(membership); // gleiche Id -> darf keine Ausnahme werfen
var reloaded = repo.GetByStudentAndGroup(membership.StudentId, membership.GroupId);
Assert.Equal(MembershipPeriod.H1Only, reloaded?.Period);
}
// ── ExamRepository ────────────────────────────────────────────────────────
[Fact]
public void ExamRepository_Delete_LoeschtZugehoerigeErgebnisseMit()
{
using var db = NewInMemoryContext();
var repo = new ExamRepository(db);
var exam = new Exam { GroupId = Guid.NewGuid() };
repo.Save(exam);
var resultId = Guid.NewGuid();
db.ExamResults.Insert(new ExamResult { Id = resultId, ExamId = exam.Id, StudentId = Guid.NewGuid() });
repo.Delete(exam.Id);
Assert.Null(repo.GetById(exam.Id));
Assert.Null(db.ExamResults.FindById(resultId));
}
[Fact]
public void ExamRepository_GetByGroup_SortiertAbsteigendNachDatum()
{
using var db = NewInMemoryContext();
var repo = new ExamRepository(db);
var groupId = Guid.NewGuid();
repo.Save(new Exam { GroupId = groupId, Title = "Früh", Date = new DateOnly(2025, 9, 1) });
repo.Save(new Exam { GroupId = groupId, Title = "Spät", Date = new DateOnly(2025, 11, 1) });
var result = repo.GetByGroup(groupId);
Assert.Equal("Spät", result[0].Title);
Assert.Equal("Früh", result[1].Title);
}
// ── SubjectRepository ─────────────────────────────────────────────────────
[Fact]
public void SubjectRepository_Save_TrimmtNamenUndKuerzel()
{
using var db = NewInMemoryContext();
var repo = new SubjectRepository(db);
var subject = new Subject { Name = " Chemie ", ShortName = " Ch " };
repo.Save(subject);
var reloaded = repo.GetById(subject.Id);
Assert.Equal("Chemie", reloaded?.Name);
Assert.Equal("Ch", reloaded?.ShortName);
}
[Fact]
public void SubjectRepository_Save_LehntLeerenNamenAb()
{
using var db = NewInMemoryContext();
var repo = new SubjectRepository(db);
Assert.Throws<ArgumentException>(() => repo.Save(new Subject { Name = " " }));
}
[Fact]
public void SubjectRepository_Save_LehntDuplikatUnabhaengigVonGrossKleinschreibungAb()
{
using var db = NewInMemoryContext();
var repo = new SubjectRepository(db);
repo.Save(new Subject { Name = "Chemie" });
Assert.Throws<InvalidOperationException>(() => repo.Save(new Subject { Name = "chemie" }));
}
[Fact]
public void SubjectRepository_Delete_WirdBlockiertWennGruppeDasFachVerwendet()
{
using var db = NewInMemoryContext();
var subjectRepo = new SubjectRepository(db);
var subject = new Subject { Name = "Chemie" };
subjectRepo.Save(subject);
db.Groups.Insert(new LearningGroup { Name = "Chemie G", SchoolYear = "2025/26", SubjectId = subject.Id });
Assert.Throws<InvalidOperationException>(() => subjectRepo.Delete(subject.Id));
}
[Fact]
public void SubjectRepository_Delete_FunktioniertWennUnbenutzt()
{
using var db = NewInMemoryContext();
var repo = new SubjectRepository(db);
var subject = new Subject { Name = "Chemie" };
repo.Save(subject);
repo.Delete(subject.Id);
Assert.Null(repo.GetById(subject.Id));
}
// ── GradingSchemeRepository ───────────────────────────────────────────────
[Fact]
public void GradingSchemeRepository_UnterscheidetGruppenspezifischesSchemaVonVoreinstellung()
{
using var db = NewInMemoryContext();
var repo = new GradingSchemeRepository(db);
var groupId = Guid.NewGuid();
repo.Save(new GradingScheme { GroupId = groupId, ExamsPercent = 60, ParticipationPercent = 30, OtherPercent = 10 });
repo.Save(new GradingScheme { GroupType = GroupType.Class, ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 });
var forGroup = repo.GetByGroup(groupId);
var defaultForClass = repo.GetDefaultForType(GroupType.Class);
var defaultForCourse = repo.GetDefaultForType(GroupType.Course);
Assert.Equal(60, forGroup?.ExamsPercent);
Assert.Equal(50, defaultForClass?.ExamsPercent);
Assert.Null(defaultForCourse);
}
// ── ReportGradeRepository ─────────────────────────────────────────────────
[Fact]
public void ReportGradeRepository_GetByStudentGroupPeriod_UnterscheidetZeitraeume()
{
using var db = NewInMemoryContext();
var repo = new ReportGradeRepository(db);
var studentId = Guid.NewGuid();
var groupId = Guid.NewGuid();
repo.Save(new ReportGrade { StudentId = studentId, GroupId = groupId, Period = "1. Halbjahr", CalculatedValue = "2" });
repo.Save(new ReportGrade { StudentId = studentId, GroupId = groupId, Period = "2. Halbjahr", CalculatedValue = "3" });
var h1 = repo.GetByStudentGroupPeriod(studentId, groupId, "1. Halbjahr");
var h2 = repo.GetByStudentGroupPeriod(studentId, groupId, "2. Halbjahr");
var unbekannt = repo.GetByStudentGroupPeriod(studentId, groupId, "Gesamtes Schuljahr");
Assert.Equal("2", h1?.CalculatedValue);
Assert.Equal("3", h2?.CalculatedValue);
Assert.Null(unbekannt);
}
// ── ParticipationSessionRepository ───────────────────────────────────────
[Fact]
public void ParticipationSessionRepository_Delete_LoeschtZugehoerigeEintraegeMit()
{
using var db = NewInMemoryContext();
var repo = new ParticipationSessionRepository(db);
var session = new ParticipationSession { GroupId = Guid.NewGuid() };
repo.Save(session);
var entryId = Guid.NewGuid();
db.ParticipationEntries.Insert(new ParticipationEntry { Id = entryId, SessionId = session.Id, StudentId = Guid.NewGuid() });
repo.Delete(session.Id);
Assert.Null(repo.GetById(session.Id));
Assert.Null(db.ParticipationEntries.FindById(entryId));
}
[Fact]
public void ParticipationEntry_EindeutigerIndex_LehntZweitenEintragFuerGleicheSitzungUndSchuelerAb()
{
using var db = NewInMemoryContext();
var repo = new ParticipationRepository(db);
var sessionId = Guid.NewGuid();
var studentId = Guid.NewGuid();
repo.Save(new ParticipationEntry { SessionId = sessionId, StudentId = studentId });
Assert.Throws<LiteDB.LiteException>(() =>
db.ParticipationEntries.Insert(new ParticipationEntry { SessionId = sessionId, StudentId = studentId }));
}
}