Summary#
TestClock.Advance(delta) moves the test's ambient clock forward by a TimeSpan, and it moves every clock the app
reads. An app reads time in three places, and a test that has said what time it is is believed by all three:
| where the time is read | does Advance move it? |
|---|---|
| the server, in memory — a function body | yes |
the server, inside a query — Item.Where(i => DateTime.UtcNow < i.Expires) | yes — the platform's instant is bound into the SQL |
the browser — a client expression or a live var computed over a list the client holds | yes |
So a row expiring in an hour is expired after Advance(2h) — in a function body, through a Where(…), and on the
page — because they are three renderings of one question and a test may only get one answer to it.
⚑ This matters most at a BOUNDARY, which is the only place it can bite. "Is it due yet" is wrong for rows either side of one instant and right for everything else, so a comparison off by an hour looks perfect until the hour that decides it. Write the two tests that pin both sides of that instant — compute the distance to the boundary and step one minute short of it, then one minute past — rather than one that advances a day and checks. Whether the second crosses midnight otherwise depends on the hour the suite happens to run.
⚠ In PRODUCTION a query's clock is still the database's now(). Nothing here changes a deployed request, which
has no pinned clock; the binding happens only when a test has pinned one.
⚠ It is TEST-ONLY, enforced by the compiler: app code that moved its own clock would have no deadlines left, and a milestone that breaches because the code said so is not an SLA. Workflow SLA clocks (reminders, the per-state expire, the whole-instance deadline) are wall-time, so advancing the clock past a timer's due moment and then calling <span class="planned" title="this page is planned and not written yet">Workflow.Settle</span> fires that timer deterministically — no real waiting.
Signature#
TestClock.Advance(<TimeSpan>); // e.g. TestClock.Advance(TimeSpan.FromHours(2))Description#
Time in a workflow SLA advances as wall-clock while a run waits. In a test that is not real time — you drive it.
TestClock.Advance adds delta to the run's clock (starting from the current instant, or now if the clock was never
pinned). The move alone changes nothing; the following Workflow.Settle(entity) runs the timer sweep, which fires
every clock now due:
- a reminder whose
After(first fire) orThenEvery(repeat) delay has elapsed runs itsRemind <Name>(…) { }body and records aRemindedrow on the audit timeline — a reminder never transitions the run; - the instance
Deadlineor a stateExpirewhose budget has elapsed fires itson Deadline/on Expireroute (which maygotoa new state).
One Settle fires each due timer once; a recurring reminder fires once per Settle, matching how a periodic worker
sweeps. Because a reminder has no state change to observe, assert on the audit timeline (see For(entity).Audit).
The delta must be a TimeSpan — durations are always TimeSpan.From…, never a bare literal.
Examples#
⭐ The commonest use, and the one this verb exists for: a state DERIVED FROM ELAPSED TIME. Watering, renewals,
trials, overdue — anything where "has enough time passed?" is the app's whole question. Without Advance a test can
only back-date the seed, which never exercises the transition the user actually cares about.
entity Lamp {
[MaxLength(80)] string Tower; DateTime LastServiced; int EveryDays = 7;
security { allow read, create, update, delete when IsAnonymous || IsAuthenticated; }
}
[Page("/")]
[AllowAnonymous]
[Render(CSR)]
component Home() {
live var lamps = Lamp.ToList();
live var due = lamps.Where(l => (DateTime.UtcNow - l.LastServiced).Days >= l.EveryDays).ToList();
render { Stack(gap: 2) { Text("Due now"); foreach (var l in due) { Text(l.Tower); } } }
}
[Test]
void a_lamp_falls_due_after_its_interval() {
var l = new Lamp { Tower = "Skerryvore", LastServiced = DateTime.UtcNow, EveryDays = 7 };
UnitOfWork.Commit();
Ui.Visit("/");
Assert.Hidden("Skerryvore"); // just serviced — not due
TestClock.Advance(TimeSpan.FromDays(8)); // the jump the keeper waits a week for
Assert.Visible("Skerryvore"); // …and the CLIENT's live var sees it
}⚑ That due is a client live var over rows the browser already holds — the third row of the table above —
and Advance moves it. This example is compiled and run by the docs gate, so it cannot go stale.
The workflow case, where the jump makes a TIMER due and the next Settle fires it:
// A FRAGMENT, deliberately: it needs a workflow, its slot and its SLA clocks to stand around it. The compiled
// version of exactly this — advance, settle, assert on the timeline — is on [Remind (milestone reminders)](/reference/workflow/remind/).
var po = new PurchaseOrder { Title = "Laptops", Total = 20000 };
Workflow.Settle(po); // autostart → arms the slot's SLA clocks
PoApproval.RaiseSubmit(po);
TestClock.Advance(TimeSpan.FromHours(2)); // past the slot's Remind Nudge(After = 1h)
Workflow.Settle(po); // sweep fires the reminder
var audit = PoApproval.For(po).Audit; // assert on the timeline, not on a state change
Assert.Equal(1, audit.Where(a => a.Kind == AuditKind.Reminded && a.Slot == "Legal").Count());
Assert.Equal(PoStatus.Review, po.Status); // a reminder does NOT transition — still waitingSee also#
- For(entity).Audit — the timeline a fired reminder writes to
- [Test] / [TestFixture] — the
[Test]a clock-driven assertion lives in