Define the observable contract
The capstone combines several explicit contracts without hiding them behind real infrastructure. ReservationBook has a positive capacity, exact ordinal code identity, a closing instant supplied with TimeProvider, and a notice sink. A request is accepted only when its code is nonblank and unused, its quantity is positive, the deadline has not arrived, and enough capacity remains. Rejected requests must not change used capacity or emit notices. The model is sequential in-memory code; it is not a concurrent database transaction or a claim that recorded notices reached an external recipient.
Walk through the real program
The first request reserves two units under A and succeeds. A repeated A request is rejected; B with two units exceeds the remaining one and is rejected. B with one unit succeeds, filling the original book. Expiration is tested on a separate fresh book with unused capacity and its own sink, after advancing the shared controlled clock exactly to its closing instant. That request is rejected solely by the time rule. The final original-book report is used three and notices two. The complete output therefore has true, false, false, true, false for its labeled requests, followed by used:3;notices:2.
Why the design is testable
Independent setup is essential here. Testing expiration only on the already full original book would be confounded: removing the expiry guard could still yield rejection because capacity fails. The fresh book gives the expired request a valid code, positive quantity, and sufficient capacity, isolating the time boundary. The same principle applies to the duplicate and excess-capacity cases: arrange other prerequisites valid when attributing a rejection to one rule. A correct-looking boolean is weak evidence if another condition could have produced it for the wrong reason.
A boundary that matters
Capacity is checked as quantity greater than capacity minus Used before addition, avoiding a careless overflowing sum in this bounded invariant. Notices occur after the in-memory mutation, but a failing real sink would require an explicit consistency or retry policy; the sample does not roll back automatically. Concurrent requests also need a synchronization or storage design not shown here. Keep those boundaries honest. A recording fake validates which notices this code requests, while separate integration tests must address the actual notifier, persistence, and multi-client behavior.
Practice: isolate one behavior
First test a fresh book immediately before its deadline, exactly at it, and immediately after it, each with unused capacity and a distinct valid code. Predict acceptance only before. Hint: advancing the clock is sufficient for this wall-clock rule; no sleep or timer is needed. Then replace the expiry comparison with a deliberately strict greater-than guard and verify that the exact-deadline case detects the defect. Use independent books or reset fixtures explicitly so an earlier accepted reservation cannot fill capacity and conceal the intended time assertion.
Practice: challenge another assumption
For a second exercise, test blank code, zero quantity, duplicate code, and excessive quantity on otherwise valid fresh scenarios. Record Used, reservation membership if exposed through a supported query, and the sink's messages before and after each rejection. Hint: no-effect obligations are part of the contract, not an incidental implementation detail. Then introduce a deliberately throwing notifier and decide the documented failure policy before coding a fix. Do not claim the current in-memory method already provides atomic delivery or safely retries an external effect.
Review the evidence
A useful capstone report names the contract, controlled dependencies, independent cases, observed state, effects, and remaining integration limits. The example connects earlier lessons rather than replacing them with one large happy path. Preserve the concrete expectations and ensure each negative scenario reaches the rule its name describes. In the accompanying multi-file labs, domain checks and JSON-boundary checks remain separately reviewable. This is reliable testing design in practice: make both the evidence and its limitations precise enough that a plausible wrong implementation would actually be rejected.
- The expiration check uses a fresh book so capacity cannot mask the time rule.
- Rejected requests must preserve both state and the recorded effect count.
- The capstone remains an in-memory sequential workflow with explicit integration limits.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
var initial = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
var clock = new ManualClock(initial);
var sink = new RecordingSink();
var book = new ReservationBook(3, initial.AddMinutes(1), clock, sink);
Console.WriteLine($"first:{book.TryReserve("A", 2)}");
Console.WriteLine($"duplicate:{book.TryReserve("A", 1)}");
Console.WriteLine($"excess:{book.TryReserve("B", 2)}");
Console.WriteLine($"last-unit:{book.TryReserve("B", 1)}");
var expiring = new ReservationBook(3, initial.AddMinutes(1), clock, new RecordingSink());
clock.Advance(TimeSpan.FromMinutes(1));
Console.WriteLine($"expired:{expiring.TryReserve("C", 1)}");
Console.WriteLine($"used:{book.Used};notices:{sink.Messages.Count}");
sealed class ManualClock : TimeProvider
{
private DateTimeOffset now;
public ManualClock(DateTimeOffset initial) { now = initial; }
public override DateTimeOffset GetUtcNow() => now;
public void Advance(TimeSpan amount) { if (amount < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(amount)); now += amount; }
}
interface INoticeSink { void Send(string message); }
sealed class RecordingSink : INoticeSink
{
public List<string> Messages { get; } = new();
public void Send(string message) => Messages.Add(message);
}
sealed class ReservationBook
{
private readonly int capacity;
private readonly DateTimeOffset closesAt;
private readonly TimeProvider clock;
private readonly INoticeSink sink;
private readonly Dictionary<string, int> reservations = new(StringComparer.Ordinal);
public int Used { get; private set; }
public ReservationBook(int capacity, DateTimeOffset closesAt, TimeProvider clock, INoticeSink sink)
{
if (capacity < 1) throw new ArgumentOutOfRangeException(nameof(capacity));
this.capacity = capacity; this.closesAt = closesAt; this.clock = clock ?? throw new ArgumentNullException(nameof(clock)); this.sink = sink ?? throw new ArgumentNullException(nameof(sink));
}
public bool TryReserve(string code, int quantity)
{
if (string.IsNullOrWhiteSpace(code) || quantity < 1 || reservations.ContainsKey(code) || clock.GetUtcNow() >= closesAt || quantity > capacity - Used) return false;
reservations.Add(code, quantity); Used += quantity;
sink.Send("reserved:" + code);
return true;
}
}
Expected output
first:True duplicate:False excess:False last-unit:True expired:False used:3;notices:2
Check each rejection preserves state and effects; this in-memory workflow is not a concurrent transaction system.