Test time
Some behavior depends on time, for example tariffs and expiry. A test that sleeps through that time is slow and flaky. ProtoTest gives every test a clock you move by hand, and the application under test reads the same clock.
Seeding the run
builder.ConfigureClock(new ProtoClock(new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero)));
Every test's clock starts where the run clock points, so tests begin from the same fixed instant. Without this call the run starts at the current real time.
Advancing a test
[ProtoTest]
public async Task AnOverstayingCarIsChargedAnIdleFee()
{
var session = await StartSessionAsync(energyKwh: 22);
Proto.Context.Clock.Advance(TimeSpan.FromHours(5));
using var invoice = await Proto.Context.Rest().GetAsync($"/api/invoices/{session.InvoiceId}");
invoice.Should.MatchShape(new { idleFee = JsonValue.GreaterThan(0) });
}
Proto.Context.Clock is this test's ProtoClock: Advance(delta) moves it forward, SetUtcNow(instant) moves it anywhere. Each test starts from the run's seed, so advancing time in one test never leaks into another, and parallel tests keep separate timelines.
| Clock | Moves when |
|---|---|
Proto.Context.Clock | the test advances it with Advance or SetUtcNow |
ProtoHost.CurrentHost.Clock | the run advances it; workers with no test see this clock |
What the application sees
An in-process ASP.NET Core application has its TimeProvider replaced with the run's, and each request carries the id of the test that caused it, so application code that injects TimeProvider sees exactly the time the test set, without knowing about ProtoTest:
public sealed class TariffService(TimeProvider timeProvider)
{
public decimal Calculate(...) => ... timeProvider.GetUtcNow() ...;
}
Two things must be true for this to work: the application resolves TimeProvider from dependency injection (not DateTime.UtcNow, and not a cached TimeProvider.System), and the request comes from the test's own client. The lookup is scoped to the host that owns the test, so two hosts configured with the same RunPrefix each link their requests to their own clock.
A worker host runs on background flows with no test, so it sees the run clock. Advance the run clock to move a worker's time:
ProtoHost.CurrentHost.Clock.Advance(TimeSpan.FromDays(1));
The bridge exists only where the run hosts the process: the in-process application server and a hosted worker. A loopback, container, AppHost or published application resolves its own TimeProvider, so the run cannot move its time. [RequiresTestClock] skips the journeys that need otherwise.
What the trace records
Advancing a test's clock records a clock entity with its new value and writes a clock.advance event carrying the delta and both instants. Advancing the run clock records the same event on the run. Trace entries need tracing enabled: a suite that turned entries off keeps the entity state only.
Limits
- Only
GetUtcNowis virtual. Timers created from this provider still run on real time. Code that schedules withTimeProvider.CreateTimerorPeriodicTimeris not accelerated. - Direct wall-clock calls are not affected.
DateTime.UtcNow,DateTimeOffset.UtcNowandEnvironment.TickCountbypass the clock. Application code must read itsTimeProvider. - Published environments cannot be faked. A deployed process keeps its own time, so time-dependent journeys are in-process journeys. Guard them with
[RequiresTestClock](theclockcapability the winning in-process provider declares),[RequiresInProcess]or a capability skip. - An application that caches time fails the same way it would in production. A service that resolves the clock once and stores a value it computed earlier stays stale. The fake clock makes that visible rather than causing it.
- A pushed clock outlives its request in flows that captured it.
ProtoRequestClock.Pushrestores the previous clock when its scope ends, but it cannot revoke the value from a task that captured it: a fire-and-forget task started inside a request keeps the finished test's clock. Long-lived background work must read the run clock. - The clock is per test, the run is per suite. Advancing
Proto.Context.Clockdoes not touch infrastructure or workers. UseProtoHost.CurrentHost.Clockwhen the whole run should move.