DI replacement with AddPrimeTestClock (testing)
In tests you typically want application code to run unchanged but receive a virtual clock. Both PrimeTime testing packages add an AddPrimeTestClock extension method that registers a single PrimeTestClock as the implementation for IPrimeTestClock, IPrimeClock, and IPrimeTime, so any consumer of the regular abstractions resolves the same instance the test controls.
The two stacks differ only in the clock construction style and the "now" surfaces they verify:
- System Clock stack stays on
DateTimeOffsetandUtcNowDateTimeOffset. - PrimeTime (NodaTime) stack uses
InstantandNowInstant.
System Clock stack
/// <summary>
/// Verifies DI consumers receive the same <see cref="PrimeTestClock"/> instance for
/// <see cref="IPrimeTestClock"/>, <see cref="IPrimeClock"/>, and <see cref="IPrimeTime"/>.
/// </summary>
[Fact]
public void DependencyInjection_AddPrimeTestClock_ResolvesSingletonAbstractions ()
{
ServiceCollection services = new();
services.AddPrimeTestClock();
services.AddSingleton<VirtualUtcTimestampService>();
using ServiceProvider provider = services.BuildServiceProvider();
VirtualUtcTimestampService consumer = provider.GetRequiredService<VirtualUtcTimestampService>();
IPrimeTestClock testClock = provider.GetRequiredService<IPrimeTestClock>();
IPrimeClock clock = provider.GetRequiredService<IPrimeClock>();
DateTimeOffset marker = new(2025, 4, 1, 15, 0, 0, TimeSpan.Zero);
testClock.SetTime(marker);
consumer.ReadUtc().Should().Be(marker);
clock.UtcNowDateTimeOffset.Should().Be(marker);
consumer.ReadUtc().Should().Be(testClock.UtcNowDateTimeOffset);
}
/// <summary>
/// Sample consumer that reads the virtual UTC time from an injected <see cref="IPrimeClock"/>.
/// </summary>
private sealed class VirtualUtcTimestampService
{
private readonly IPrimeClock _clock;
/// <summary>
/// Initializes a new instance of the <see cref="VirtualUtcTimestampService"/> class.
/// </summary>
/// <param name="clock">The prime clock abstraction.</param>
public VirtualUtcTimestampService (IPrimeClock clock)
{
_clock = clock;
}
/// <summary>
/// Reads the clock's current virtual UTC time.
/// </summary>
/// <returns>The virtual UTC <see cref="DateTimeOffset"/>.</returns>
public DateTimeOffset ReadUtc () =>
_clock.UtcNowDateTimeOffset;
}
PrimeTime (NodaTime) stack
/// <summary>
/// Verifies DI consumers receive the same <see cref="PrimeTestClock"/> instance for
/// <see cref="IPrimeTestClock"/>, <see cref="IPrimeClock"/>, and <see cref="IPrimeTime"/>.
/// </summary>
[Fact]
public void DependencyInjection_AddPrimeTestClock_ResolvesSingletonAbstractions ()
{
ServiceCollection services = new();
services.AddPrimeTestClock();
services.AddSingleton<VirtualTimestampService>();
using ServiceProvider provider = services.BuildServiceProvider();
VirtualTimestampService consumer = provider.GetRequiredService<VirtualTimestampService>();
IPrimeTestClock testClock = provider.GetRequiredService<IPrimeTestClock>();
IPrimeClock clock = provider.GetRequiredService<IPrimeClock>();
Instant marker = Instant.FromUtc(2025, 4, 1, 15, 0, 0);
testClock.SetInstant(marker);
consumer.ReadInstant().Should().Be(marker);
clock.NowInstant.Should().Be(marker);
consumer.ReadInstant().Should().Be(testClock.NowInstant);
}
/// <summary>
/// Sample consumer that reads the virtual instant from an injected <see cref="IPrimeClock"/>.
/// </summary>
private sealed class VirtualTimestampService
{
private readonly IPrimeClock _clock;
/// <summary>
/// Initializes a new instance of the <see cref="VirtualTimestampService"/> class.
/// </summary>
/// <param name="clock">The prime clock abstraction.</param>
public VirtualTimestampService (IPrimeClock clock)
{
_clock = clock;
}
/// <summary>
/// Reads the clock's current instant.
/// </summary>
/// <returns>The virtual UTC instant exposed by the clock.</returns>
public Instant ReadInstant () =>
_clock.NowInstant;
}
What to notice (both stacks)
services.AddPrimeTestClock()registers onePrimeTestClockas the singleton implementation behind the test, production, and "time" abstractions. CallingtestClock.SetTime(...)/testClock.SetInstant(...)is observed by every consumer that resolvedIPrimeClock.- The sample
*TimestampServiceconsumer accepts the production abstraction (IPrimeClock) — it is not test-aware. That is the property that lets production code stay unchanged while tests inject virtual time. - After
SetInstant/SetTime, every read ofNowInstant/UtcNowDateTimeOffsetreturns the set instant until the test advances the clock. WhileStart()is active, "now" may project between commits and persist-on-read may dispatch due work — see Test-clock control APIs.
Related
- Test-clock control APIs —
Set*,Advance,RunFor,Start/Stoppatterns shared by both stacks. - System Clock track: DI registration (production)
- PrimeTime track: DI registration (production)
- API:
KZDev.SystemClock.PrimeTime.Testing·KZDev.PrimeTime.Testing