Table of Contents

Interval timer (testing) — PrimeTime (NodaTime) stack

These xUnit examples drive interval timers under a virtual PrimeTestClock. Callbacks fire only when the test advances the clock, so the assertions are deterministic and there are no real-world wait times.

/// <summary>
/// Verifies a one-shot interval timer fires after virtual time advances past the initial delay.
/// </summary>
[Fact]
public void IntervalTimer_AdvanceOneShot_FiresOnce ()
{
    IPrimeTestClock clock = new PrimeTestClock(Instant.FromUtc(2025, 1, 1, 0, 0, 0));
    int fired = 0;
    using IClockIntervalTimer registration = clock.RegisterTimer(
        TimeSpan.FromMinutes(5),
        Timeout.InfiniteTimeSpan,
        _ => fired++,
        CancellationToken.None);

    clock.Advance(Duration.FromMinutes(4));
    fired.Should().Be(0);
    clock.Advance(Duration.FromMinutes(1));
    fired.Should().Be(1);
    registration.IsRepeating.Should().BeFalse();
}

/// <summary>
/// Verifies a repeating interval timer advances its schedule deterministically under
/// <see cref="IPrimeTestClock.Advance(NodaTime.Duration)"/>.
/// </summary>
[Fact]
public void IntervalTimer_AdvanceRepeating_FiresOnEachInterval ()
{
    IPrimeTestClock clock = new PrimeTestClock(Instant.FromUtc(2025, 1, 1, 0, 0, 0));
    int fired = 0;
    using IClockIntervalTimer registration = clock.RegisterTimer(
        TimeSpan.FromMinutes(2),
        TimeSpan.FromMinutes(3),
        _ => fired++,
        CancellationToken.None);

    clock.Advance(Duration.FromMinutes(2));
    fired.Should().Be(1);
    clock.Advance(Duration.FromMinutes(3));
    fired.Should().Be(2);
    registration.IsRepeating.Should().BeTrue();
}

/// <summary>
/// Verifies an asynchronous interval callback completes when virtual time reaches the due instant.
/// </summary>
[Fact]
public void IntervalTimer_RegisterAsyncTimer_Advance_CompletesCallback ()
{
    IPrimeTestClock clock = new PrimeTestClock(Instant.FromUtc(2025, 1, 1, 0, 0, 0));
    int fired = 0;
    using IClockIntervalTimer registration = clock.RegisterAsyncTimer(
        TimeSpan.FromSeconds(1),
        Timeout.InfiniteTimeSpan,
        async (_, cancellationToken) =>
        {
            fired++;
            await Task.Delay(0, cancellationToken).ConfigureAwait(false);
        },
        CancellationToken.None);

    clock.Advance(Duration.FromSeconds(1));
    fired.Should().Be(1);
    registration.State.Should().Be(TimerState.Completed);
}

What to notice

  • PrimeTestClock is constructed from a NodaTime Instant so the NodaTime stack starts at a known UTC instant without timezone ambiguity.
  • Advance(Duration) moves virtual time forward; callbacks scheduled at or before the new "now" are dispatched synchronously by the test clock before Advance returns.
  • A one-shot timer is requested by passing Timeout.InfiniteTimeSpan as the repeat interval. IClockIntervalTimer.IsRepeating is false for one-shot registrations.
  • For repeating timers, advancing past each multiple of the repeat interval produces one callback per interval crossed.
  • The async overload (RegisterAsyncTimer) completes the awaited callback before Advance returns.