Move the test clock
A billing period takes a month. This test closes one in about 230 ms. The application runs on the test clock, and the test moves it.
- Move a test clock instead of waiting on real time.
- Explain how the in-process application reads the same clock.
- Find the clock event and the period close in the trace.
- Level 2 (when not to compose).
- The sample cloned. Reading the archive alone also works.
The scenario
A subscription renews every billing period. A test for the due window has to place the application a period later, and a test that sleeps through that time is slow on a good day and wrong on a bad one.
The sample hosts the API in-process, and that server hands the application the test's clock. ClockJourney asks the application where the period ends, moves the clock just past that instant, and checks the invoice the application issued.
One clock, two readers
The composition starts the API in-process. That server is what carries the test clock into the application:
1builder.AddApplication(NorthstarTargets.Api, app =>2{3if (run.RunsLocalApplications)4{5// The in-process server is what carries the test clock into the application.6app.AddAspNetCoreServer<NorthstarProgram>(configureWebHost: webHost =>7ConfigureHostedApplication(webHost, run));8}
- The API application
Tests select it with [Application(NorthstarTargets.Api)].
- Only when the run hosts the application
A run pointed at a deployed address does not start a server, and it cannot move that application time.
- The bridge
The in-process server replaces the application's time provider with the run's, so the application computes every stamp and period from the test clock.
samples/Northstar.ProtoTest/Setup.cs. The suite also removes the domain's own time provider in its test-side composition, so the bridge wins and the application keeps the test clock.The test then moves that clock and reads what the application did with it.
The journey
1using var subscription = await Proto.Context.Rest().GetAsync("/api/v1/subscription");2var current = subscription3.Should.HaveHttpStatus(HttpStatusCode.OK)4.ReadRequired<SubscriptionResponse>();56// Move the test clock past the period end; the application reads the same clock.7Proto.Context.Clock.Advance(8current.CurrentPeriodEndUtc - Proto.Context.Clock.GetUtcNow() + TimeSpan.FromSeconds(1));910using var invoices = await Proto.Context.Rest()11.GetAsync("/api/v1/invoices", new { status = InvoiceStatuses.Open });12var invoice = invoices13.Should.HaveHttpStatus(HttpStatusCode.OK)14.ReadRequired<CursorPage<InvoiceResponse>>()15.Items16.Single();
- Ask the application for the window
The period end is the application's value, read over the composed client.
- Move the test clock
The delta is the time left in the period plus one second, so the clock lands just past the boundary.
- Read what the move produced
The application issued the invoice when the period closed, and the test reads the open one.
samples/Northstar.ProtoTest/ClockJourney.cs. The test asserts the invoice stamp and its line afterwards with ordinary NUnit assertions.What the trace recorded
From the archive:
| Entry | What 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 200 | the period the test read |
Northstar.Domain invoice.issue, reported by the application | the application closed the period |
http.request REST GET /api/v1/invoices, 68.4 ms, HTTP 200 | the invoice the test read |
test.execution, 233.7 ms | the whole journey |
The clock advance is an event on the test execution, not a request. Nothing was sent anywhere to move time: the test holds the clock, and the application reads it.
When the application does not run in-process
A loopback, container, AppHost or deployed application resolves its own time provider. The run cannot move that time, and a journey that needs the clock is an in-process journey. That is why the sample runs the API in-process for these tests and points the browser journey at a loopback instance. The test time reference names the attributes that skip a clock-dependent test when the run cannot serve it.
Checkpoint
The clock advance in the trace reads 30:0:00:01. Why does the test move the clock to the period end minus the current time plus one second, instead of a fixed number of days?
clock.advance event on the test execution.What you learned
- The test owns the clock; the in-process application reads it through its time provider.
- Moving the clock is instant, and the trace records the delta and both instants.
- A wait on real time changes nothing the application can see.