Skip to main content

Move the test clock

The problem​

Northstar issues an invoice when a request finds that a subscription's billing period has ended. Testing that boundary requires the application to read a later time.

// Waiting on real time does not move this sample's test clock.
await Task.Delay(TimeSpan.FromSeconds(2));

This delay waits two real seconds, but the sample's test clock stays where it was. Increasing the delay would not cross the billing boundary.

Instead, the test moves its clock past the period end and sends a request. The application reads that time and issues the invoice.

Do it​

1. Let the application read the test's clock​

Use the sample's default local configuration for this lesson. Follow ClosingTheBillingPeriodIssuesTheInvoiceOnTheTestClock in samples/Northstar.ProtoTest/ClockJourney.cs, or inspect the linked recording.

The setup class starts the API inside the test process. In AddApplication, where the setup class registers the API, this excerpt adds the in-process server:

app.AddAspNetCoreServer<NorthstarProgram>(configureWebHost: webHost =>
ConfigureHostedApplication(webHost, run));

The sample already configures this in Setup.cs. The server supplies a TimeProvider that reads the current test's clock while handling its requests.

Northstar's billing code uses that provider. Code that reads DateTimeOffset.UtcNow directly would still see real time.

2. Ask the application where the period ends​

using var subscription = await Proto.Context.Rest().GetAsync("/api/v1/subscription");
var current = subscription
.Should.HaveHttpStatus(HttpStatusCode.OK)
.ReadRequired<SubscriptionResponse>();

The response supplies CurrentPeriodEndUtc. The test uses that boundary rather than assuming how many days the month contains.

3. Move the clock just past that moment​

// Move the test clock past the period end; the application reads the same clock.
Proto.Context.Clock.Advance(
current.CurrentPeriodEndUtc - Proto.Context.Clock.GetUtcNow() + TimeSpan.FromSeconds(1));

The delta is the time left in the period plus one second, so the clock lands beyond the boundary. Advance updates the clock without waiting for that duration to pass.

4. Read what the application did​

using var invoices = await Proto.Context.Rest()
.GetAsync("/api/v1/invoices", new { status = InvoiceStatuses.Open });
var invoice = invoices
.Should.HaveHttpStatus(HttpStatusCode.OK)
.ReadRequired<CursorPage<InvoiceResponse>>()
.Items
.Single();

This request makes Northstar evaluate the period boundary and issue the invoice. Advancing the clock alone did not execute that billing logic.

The test expects exactly one open invoice. It then checks IssuedAtUtc against current.CurrentPeriodEndUtc, rather than the advanced time one second later. It also checks the open status and a line description containing Growth.

These NUnit assertions are in samples/Northstar.ProtoTest/ClockJourney.cs.

5. Open the trace​

To run the journey yourself, use this command from the repository root:

dotnet test samples/Northstar.ProtoTest --filter "FullyQualifiedName~ClosingTheBillingPeriodIssuesTheInvoiceOnTheTestClock"

Expect one passed test in the default local configuration. Its trace appears under samples/Northstar.ProtoTest/bin/Debug/net8.0/TestResults/.

Or download l3-clock-window.prototrace and open it in the viewer. Select ClosingTheBillingPeriodIssuesTheInvoiceOnTheTestClock. The supplied recording contains:

EntryWhat it shows
clock.advance on test.execution, "Clock advanced by 30:0:00:01"the delta, from the test side
http.request REST GET /api/v1/subscription, 141.9 ms, HTTP 200the period the test read
Northstar.Domain invoice.issue, reported by the applicationthe application closed the period
http.request REST GET /api/v1/invoices, 68.4 ms, HTTP 200the invoice the test read
test.execution, 233.7 msthe test body, excluding setup and teardown

The recorded test body took 233.7 ms while its clock moved a month and one second.

What happened​

The trace records the clock advance as an event on test.execution. The test updates an in-memory clock. The next request lets the application observe the new time.

Each test gets its own clock, which starts at the run's time. Advancing one test's clock does not advance another's. On each request, the in-process server uses the test id to find the right clock.

The clock only changes TimeProvider.GetUtcNow(). Timers and delays still use real time, and an application outside the test process, such as a container, needs its own way to control time.

Check yourself​

The clock advance in the trace reads 30:0:00:01. Why does the test compute the delta from the period end instead of advancing a fixed number of days?

Verify
Select the test in the archive and find the clock.advance event on the test execution.

Remember​

  • Each test has its own clock. The in-process request reads it through TimeProvider.
  • Advancing the clock does not wait for the requested duration. The trace records the delta.
  • The next request triggers Northstar's billing logic. Sleeping does not move this test clock.

Go deeper​