Examples

Learning

School to submission, with roster numbers across sections and a clock shared with the code under test.

A late-submission test needs setup and policy to agree about time. This example shares a fake clock between them and explores why a nullable grade of zero must survive a derived default.

School → Course → Section → Enrollment → Assignment → Submission. Rosters run across a course's sections, assignments restart per enrollment, and a late policy reads the same FakeTimeProvider as the setup. Source: tests/Zeeq.Biome.Tests/Learning.

Blueprints: scopes, time, and a derived Late flag

Learning/Domain.cs
// Enrollment parents are sections; Per<Course>
// continues the roster across those sections.
internal static readonly Sequence<int> RosterNumbers = Sequence
    .Of(1, n => n + 1)
    .Per<Course>();

// Open in the past, due in the future; use ctx time for setup instead of UtcNow.
internal static readonly ChildBlueprint<
    LearningSeed,
    Enrollment,
    Assignment
> Assignment = Blueprint
    .ChildOf<LearningSeed, Enrollment, Assignment>(
        (enrollment, ctx) =>
            new()
            {
                Enrollment = enrollment,
                School = ctx.Ancestor<School>(),
                OpensAt = ctx.Ago(TimeSpan.FromDays(1)),
                DueAt = ctx.FromNow(TimeSpan.FromDays(1)),
            }
    )
    .Derive(a => a.Number, AssignmentNumbers)
    .Derive(a => a.Title, (a, _) => $"Assignment {a.Number}");

// Time precedes the Late derive; grade remains
// fill-only so a call-site grade survives.
internal static readonly ChildBlueprint<
    LearningSeed,
    Assignment,
    Submission
> Submission = Blueprint
    .ChildOf<LearningSeed, Assignment, Submission>(
        (assignment, ctx) =>
            new()
            {
                Assignment = assignment,
                School = ctx.Ancestor<School>(),
                SubmittedAt = ctx.Now,
            }
    )
    .Derive(s => s.Attempt, Attempts)
    .Derive(s => s.Grade, (_, _) => 80m)
    .Derive(s => s.Late, (s, _) => s.SubmittedAt > s.Assignment.DueAt);

A call-site grade survives the fill-only derive

A grade of zero is a meaningful result, so this model uses decimal? to distinguish it from a missing grade. The test checks that the derived default leaves zero alone and that a draft can be built without being saved.

await biome
    .AddSchool(out _)
    .AddCourse(out var course)
    .AddSections(out var sections, _ => { }, s => s.Number = 1)
    .AddEnrollments(out _, 2)
    .AddAssignments(out var assignments, 1)
    .AddSubmissions(
        out var submissions,
        Submissions.Distinction + Submissions.Reviewed
    )
    .Under(course)
    .AddSection(out var third)
    // Return to a specific assignment for a second, preview-only attempt.
    .Under(assignments[0])
    .AddSubmission(out var draft, s => s.Grade = 0m)
    .Transient()
    .BuildAsync();

The real test asserts sections[0].Number == 2, third.Number == 3, draft.Grade == 0m (explicit zero kept, not filled with 80), draft.Attempt == 2 (attempts resume per assignment), and four saved submissions out of five built.

Derive(s => s.Grade, (_, _) => 80m) fills decimal? only when it's null. Because Grade is nullable, 0m is explicit. For a non-nullable decimal, 0 would count as unset.

Roster numbers cross section boundaries

LearningTests.cs
await biome
    .AddSchools(out var schools, 2)
    .AddCourses(out _, 2) // 4 courses
    .AddSections(out var sections, 2) // 8 sections
    .AddEnrollments(out var enrollments, 3) // 24 enrollments
    .AddAssignments(out var assignments, 2) // 48 assignments
    .AddSubmissions(out var submissions, 1)
    .BuildAsync();

// sections[2].Number == 1          (numbered per course)
// enrollments[3].RosterNumber == 4 (the roster continues into the next section)
// enrollments[6].RosterNumber == 1 (a new course restarts it)
// submissions[47].School == schools[1]  (Ancestor<School> reaches five levels up)

One clock for setup and policy

The late policy and the biome read the same fake clock. Advancing it changes the policy's answer, but never mutates historical rows:

LearningTests.cs
var deadline = new DateTimeOffset(2026, 10, 11, 12, 0, 0, TimeSpan.Zero);
var time = new FakeTimeProvider(deadline.AddHours(-1));
var policy = new LatePolicy(time);
var biome = TestBiome.Start(out _, time: time);

biome.AddLateSubmission(
    out var historicalLate,
    dueAt: time.GetUtcNow().AddMinutes(-1)
);
biome
    .AddSchool(out _)
    .AddCourse(out _)
    .AddSection(out _)
    .AddEnrollment(out _)
    .AddAssignment(out var upcoming, a => a.DueAt = deadline);
var wasOverdue = policy.IsOverdue(upcoming); // false

time.Advance(TimeSpan.FromHours(2));
await biome.Under(upcoming).AddSubmission(out var late).BuildAsync();

// policy.IsOverdue(upcoming) is now true; late.Late is derived true;
// historicalLate.SubmittedAt is unchanged
A scenario helper can take an argument: AddLateSubmission(out var leaf, dueAt: …) builds the whole six-level branch with a chosen deadline, and its definition still shows every step. See Scenario helpers.