Time and events
A deadline, an order of events, or "every one of these ends" cannot be checked on a single value. These rules are about a sequence, so the test drives the real service through generated calls and judges the events it recorded.
internal sealed class Fulfilment(TimeProvider time, TimeSpan shipsAfter)
{
public IReadOnlyList<Timestamped<FulfilmentEvent>> Log => _log;
// ... Confirm() records Confirmed and Shipped, stamped from `time`
}
The service reads time only from the TimeProvider it is given. A service that calls DateTime.UtcNow or Task.Delay is invisible to a virtual clock, and a missed deadline can never happen in a test.
TemporalRuleSet<FulfilmentEvent>.From(
("confirmed-ships-within-a-minute",
Temporal<FulfilmentEvent>.AfterUntil(
e => e is FulfilmentEvent.Confirmed,
(_, next) => next is FulfilmentEvent.Shipped,
TimeSpan.FromSeconds(60))));
await EventAutoTest.For(rules, script, time => new Fulfilment(time, shipsAfter), f => f.Log).RunAsync();
script lists the operations (Script.For<Fulfilment>().Step("Confirm", f => f.Confirm())). The test builds a fresh service per run on a manual clock, advances it by generated delays, and checks the rules against the log.
- You write no delays. The test tries 0 and just either side of every rule's deadline, so a deadline is both met and missed within a few draws. A script that sets its own delays keeps them.
- A rule fails as vacuous (see concepts) if no run contained its trigger, or none went past its deadline.
- If the service exposes its events as an
IAsyncEnumerable<Timestamped<T>>, useEventAutoTest.ForAsync(...).
Nothing here depends on Reactive Extensions. If your service is built on IObservable<T> and an Rx scheduler, the opt-in package Nullean.Entities.CsCheck.Reactive has the same script on a scheduler.
Temporal failed.
Rule broken confirmed-ships-within-a-minute Noticed at +60s What happened No matching event within 00:01:00 after Confirmed Events recorded +0s Confirmed +90s Shipped Calls made wait 1s, Confirm
Law What must follow something within a deadline does, however the time between calls is spent. Consequence A deadline is missed under timing the tests never tried, so a stuck state is reported as in progress for good.
Smallest case found, after 5 shrinks. To run this case again: .Seed("0i1vmP3ChLc5")