Testland
Browse all skills & agents

dotnet-faketime

Wraps .NET's TimeProvider abstraction (System.TimeProvider, introduced .NET 8) and FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing: SetUtcNow, Advance, AutoAdvanceAmount, CreateTimer, Delay, and the pre-.NET-8 ISystemClock migration path. Use when testing C# or F# code that reads the current time, uses timers, or awaits Task.Delay.

Install with skills.sh (any agent)

npx skills add testland/qa --skill dotnet-faketime
View source

dotnet-faketime

Overview

.NET 8 introduced System.TimeProvider, an abstract class in System.Runtime.dll that provides a testable abstraction for time. The production singleton TimeProvider.System wraps DateTimeOffset.UtcNow, the local TimeZoneInfo, Stopwatch for high-frequency timestamps, and System.Threading.Timer.

For tests, FakeTimeProvider (namespace Microsoft.Extensions.Time.Testing, assembly Microsoft.Extensions.TimeProvider.Testing.dll) subclasses TimeProvider and gives full control over the fake clock.

When to use

  • C# or F# unit tests for code that calls timeProvider.GetUtcNow() or timeProvider.GetLocalNow().
  • Tests involving ITimer created via CreateTimer, or async code using timeProvider.Delay(...).
  • Migrating away from DateTime.UtcNow / DateTimeOffset.UtcNow called directly in production code.
  • Upgrading from the pre-.NET-8 ISystemClock pattern.

How to use

  1. Add the Microsoft.Extensions.TimeProvider.Testing package to the test project (Step 1).
  2. Refactor production code to take TimeProvider by constructor injection and register TimeProvider.System in DI (Step 2).
  3. In each test, construct a FakeTimeProvider and freeze the instant with SetUtcNow(...) or the constructor overload (Step 3).
  4. Drive the clock forward with Advance(TimeSpan), then assert on the behaviour (Step 4).
  5. For async code, await fakeTime.Delay(...) or CreateTimer callbacks, advancing the virtual clock to fire them (see advanced clock control).
  6. Set SetLocalTimeZone(...) when a branch reads GetLocalNow() (see advanced clock control).
  7. Register FakeTimeProvider, never TimeProvider.System, in the test DI container.

Step 1 - Add the NuGet package

FakeTimeProvider ships in a separate testing package, not in the BCL. Install it only in test projects:

<!-- In your test .csproj -->
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing"
                  Version="9.*" />

The package name is Microsoft.Extensions.TimeProvider.Testing; it ships the FakeTimeProvider type. TimeProvider itself is built into the .NET 8+ runtime and needs no extra package for production code targeting net8.0 or later.

Step 2 - Inject TimeProvider into production code

Replace direct calls to DateTime.UtcNow or DateTimeOffset.UtcNow with a constructor-injected TimeProvider. Wire TimeProvider.System in the DI container; pass FakeTimeProvider in tests.

// Production code
public class TokenService
{
    private readonly TimeProvider _time;

    public TokenService(TimeProvider time)
    {
        _time = time;
    }

    public bool IsExpired(DateTimeOffset expiresAt)
        => _time.GetUtcNow() > expiresAt;
}

// DI registration (Startup / Program.cs)
services.AddSingleton(TimeProvider.System);

Step 3 - Write a deterministic test with SetUtcNow

using Microsoft.Extensions.Time.Testing;

[Fact]
public void IsExpired_ReturnsFalse_WhenTokenNotYetExpired()
{
    var fakeTime = new FakeTimeProvider();
    fakeTime.SetUtcNow(new DateTimeOffset(2026, 5, 20, 12, 0, 0, TimeSpan.Zero));

    var svc = new TokenService(fakeTime);

    Assert.False(svc.IsExpired(new DateTimeOffset(2026, 5, 20, 13, 0, 0, TimeSpan.Zero)));
}

SetUtcNow(DateTimeOffset) sets the frozen instant. The value must be equal to or later than the current fake time; the clock cannot go backwards.

Step 4 - Advance time by a duration

Advance(TimeSpan) moves the clock forward from its current position.

[Fact]
public void IsExpired_ReturnsTrue_AfterTokenLifetime()
{
    var fakeTime = new FakeTimeProvider(
        new DateTimeOffset(2026, 5, 20, 12, 0, 0, TimeSpan.Zero));
    var svc = new TokenService(fakeTime);

    // Token valid for 1 hour
    var expiresAt = fakeTime.GetUtcNow().AddHours(1);

    fakeTime.Advance(TimeSpan.FromHours(2));  // jump past expiry

    Assert.True(svc.IsExpired(expiresAt));
}

The FakeTimeProvider(DateTimeOffset) constructor overload sets the starting instant directly.

Verify: run dotnet test and confirm the boundary assertions pass. If a test hangs or times out instead, the code under test is calling real Task.Delay or Thread.Sleep rather than the injected TimeProvider - fix the injection and re-run before relying on the suite.

Advanced clock control

For auto-advancing reads (AutoAdvanceAmount), virtual-clock timers and Task.Delay (CreateTimer / Delay), and local time zone testing (SetLocalTimeZone / GetLocalNow), see references/advanced-clock-control.md.

Migrating from ISystemClock

Code on the pre-.NET-8 ISystemClock pattern replaces that injection with TimeProvider and its fakes with FakeTimeProvider. The legacy interface, a fake implementation, and the full migration path are in references/isystemclock-migration.md.

CI integration

No special runner configuration is needed. Tests complete instantly because no wall-clock sleeping occurs.

jobs:
  dotnet-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.x'
      - run: dotnet test --configuration Release

Anti-patterns and limitations

Common misuses (direct DateTime.UtcNow, static DateTime mocks, backwards SetUtcNow, missing Task.Yield()) and the tool's limits (Task.Delay(int), Thread.Sleep, GetTimestamp(), third-party DateTime.UtcNow, no leap seconds) are cataloged in references/anti-patterns-and-limitations.md.

References

Advanced clock control

View source (opens in new window)

Advanced clock control

Beyond SetUtcNow and Advance, FakeTimeProvider controls auto-advancing reads, virtual-clock timers, and the local time zone.

Auto-advance on every read

AutoAdvanceAmount makes the clock tick forward by a fixed amount each time GetUtcNow() is called. Per learn.microsoft.com/dotnet/api/microsoft.extensions.time.testing.faketimeprovider.autoadvanceamount (opens in new window): "Gets or sets the amount of time by which time advances whenever the clock is read."

var fakeTime = new FakeTimeProvider();
fakeTime.AutoAdvanceAmount = TimeSpan.FromMilliseconds(100);

// Each GetUtcNow() call adds 100 ms
var t1 = fakeTime.GetUtcNow();
var t2 = fakeTime.GetUtcNow();
Assert.Equal(TimeSpan.FromMilliseconds(100), t2 - t1);

Use AutoAdvanceAmount for elapsed-time assertions, not for tests that need precise control - explicit Advance calls are more readable there.

Test Task.Delay and timers

FakeTimeProvider controls the virtual clock used by Delay and CreateTimer so that async waiting does not block wall-clock time in tests.

[Fact]
public async Task Poller_DoesNotFireBeforeInterval()
{
    var fakeTime = new FakeTimeProvider();
    var fired = false;

    // timeProvider.Delay(...) is an extension method from
    // System.Threading.Tasks.TimeProviderTaskExtensions
    var delayTask = fakeTime.Delay(TimeSpan.FromSeconds(30));

    _ = delayTask.ContinueWith(_ => fired = true);

    fakeTime.Advance(TimeSpan.FromSeconds(10));
    await Task.Yield();  // let continuations run
    Assert.False(fired);

    fakeTime.Advance(TimeSpan.FromSeconds(20));
    await delayTask;
    Assert.True(fired);
}

Delay(TimeProvider, TimeSpan, CancellationToken) is an extension method in System.Threading.Tasks.TimeProviderTaskExtensions. Per learn.microsoft.com/dotnet/api/system.threading.tasks.timeprovidertaskextensions.delay (opens in new window): "Creates a task that completes after a specified time interval."

CreateTimer works analogously: the callback fires only when Advance moves the virtual clock past the due time. Per learn.microsoft.com/dotnet/api/microsoft.extensions.time.testing.faketimeprovider.createtimer (opens in new window): "Creates a new ITimer instance, using TimeSpan values to measure time intervals."

Local time zone testing

Set a custom LocalTimeZone on FakeTimeProvider to test timezone-sensitive branches without touching the system clock or environment variables:

var fakeTime = new FakeTimeProvider();
fakeTime.SetUtcNow(new DateTimeOffset(2026, 3, 8, 7, 0, 0, TimeSpan.Zero));
fakeTime.SetLocalTimeZone(TimeZoneInfo.FindSystemTimeZoneById("America/New_York"));

DateTimeOffset local = fakeTime.GetLocalNow();
// local is UTC-5 or UTC-4 depending on DST; see dst-transition-reference

Per learn.microsoft.com/dotnet/api/system.timeprovider.getlocalnow (opens in new window): GetLocalNow() returns the UTC instant converted to the provider's LocalTimeZone. Use the companion dst-transition-reference for expected offset values around spring/fall transitions.

Anti-patterns and limitations

View source (opens in new window)

Anti-patterns and limitations

Anti-patterns

Anti-patternWhy it failsFix
DateTime.UtcNow directly in codeNot injectable; test is forced to use wall-clockInject TimeProvider; call _time.GetUtcNow()
DateTimeOffset.UtcNow directly in codeSame problemInject TimeProvider
Static mock of DateTime via Fakes/HarmonyRequires special test runner config or IL rewritingUse DI with TimeProvider
Call SetUtcNow with a value earlier than currentThrows ArgumentOutOfRangeExceptionUse Advance or construct fresh FakeTimeProvider
Forget await Task.Yield() after AdvanceContinuations haven't had a chance to run yetYield or await the completed task
AutoAdvanceAmount in tests needing exact instantsClock shifts unexpectedly between readsSet AutoAdvanceAmount = TimeSpan.Zero (default)
Register TimeProvider.System in test DITests become time-dependent and flakyRegister FakeTimeProvider in test DI setup

Limitations

  • Task.Delay(int) overloads that do NOT accept a TimeProvider still use wall-clock time. Always use the timeProvider.Delay(TimeSpan) extension form.
  • Thread.Sleep is not controlled by FakeTimeProvider; restructure to use await timeProvider.Delay(...) instead.
  • High-frequency GetTimestamp() values are derived from the fake UTC instant, not from Stopwatch. Per learn.microsoft.com/dotnet/api/microsoft.extensions.time.testing.faketimeprovider.timestampfrequency (opens in new window), TimestampFrequency is a fixed value tied to the fake clock.
  • Third-party libraries that call DateTime.UtcNow internally are not affected by FakeTimeProvider; only code that accepts TimeProvider by injection can be controlled this way.
  • No leap-second simulation; see leap-second-reference.

Pre-.NET-8 pattern: ISystemClock migration

View source (opens in new window)

Pre-.NET-8 pattern: ISystemClock migration

Before TimeProvider, the Microsoft.Extensions stack used ISystemClock (namespace Microsoft.Extensions.Internal, assembly Microsoft.Extensions.Caching.Abstractions.dll). Per learn.microsoft.com/dotnet/api/microsoft.extensions.internal.isystemclock (opens in new window): "Abstracts the system clock to facilitate testing." It exposed a single property, UtcNow, and carried the notice "This API supports the .NET infrastructure and is not intended to be used directly from your code."

// Legacy pattern (pre-.NET 8)
public interface ISystemClock
{
    DateTimeOffset UtcNow { get; }
}

// Test implementation
public class FakeSystemClock : ISystemClock
{
    public DateTimeOffset UtcNow { get; set; }
}

Migration path: replace ISystemClock injection with TimeProvider, and replace fake implementations with FakeTimeProvider. The ISystemClock approach covers only UtcNow; TimeProvider also covers high-frequency timestamps and timers, making it the complete replacement.

Related skills

dst-transition-reference

Pure-reference catalog of Daylight Saving Time (DST) transition patterns and their canonical bug classes. Covers the spring-forward (skipped hour: 02:00 → 03:00 local) and fall-back (repeated hour: 02:00 → 01:00 local) transitions, the historical irregularity of DST (different jurisdictions, transitions on different dates, some regions abolish DST or never adopted it), the IANA timezone database (tz / Olson DB) as the canonical source, and the testable behaviors DST creates (duplicate / missing local timestamps, cron jobs that fire 0 or 2 times, billing periods that miss / double-count, recurring meetings on transition days). Per-jurisdiction DST-rule tables and refreshable per-region test-data fixtures live in references/. Use when designing or auditing time-handling code or test cases.

freezegun-python

Wraps freezegun (github.com/spulec/freezegun), the Python time-mocking library: @freeze_time decorator / context manager, freeze_time(...).start() + stop(), tick / move_to / tz_offset, and integration with datetime.now / time.time / time.localtime. Use when testing Python code that calls datetime / time.

iso-8601-vs-rfc-3339-reference

Pure-reference catalog of the ISO 8601 vs RFC 3339 distinction. Covers the relationship (RFC 3339 is a strict subset of ISO 8601 designed for internet protocols), the syntactic differences (RFC 3339 disallows ISO 8601's '+02' offset short-form requires '+02:00'; RFC 3339 mandates a date-time separator T or space; ISO 8601 allows much more), the canonical date-time string format (YYYY-MM-DDTHH:MM:SS[.fff]±HH:MM or Z), per-language parser behaviour (Python isoformat, Java Instant.parse, JS Date.parse non-spec), and serialisation rules for APIs. Use when choosing a wire format, parsing third-party datetimes, or auditing time-string handling.

jest-fake-timers

Wraps Jest's built-in modern fake-timers (built on @sinonjs/fake-timers since Jest 27): jest.useFakeTimers(), jest.setSystemTime(), jest.advanceTimersByTime(), jest.runAllTimers(), and jest.useRealTimers() for selective restoration. Use when testing JS/TS code in Jest where setTimeout / setInterval / Date / Date.now need deterministic control.

leap-second-reference

Pure-reference catalog of leap-second mechanics and the bugs they cause: the 23:59:60 UTC insertion (announced ~6 months ahead by IERS Bulletin C; 27 inserted 1972-2016; abolished by 2035 per CGPM 2022), the Google/AWS leap-smear alternative, and the four bug classes a real insertion exposes - time_t non-monotonicity, negative durations, NTP cascading, and cross-node clock skew - each with a monotonic-clock fix and a freezegun simulation. Use when auditing time-sensitive code (financial timestamping, distributed logs, NTP-driven schedulers) for second-by-second progress assumptions; for the far more common daylight-saving-time transition hazards, use dst-transition-reference instead.

libfaketime-c

Wraps libfaketime (github.com/wolfcw/libfaketime), the LD_PRELOAD library that fakes the clock for any binary by intercepting time() / gettimeofday() / clock_gettime(). Covers absolute-date mode (FAKETIME='2026-12-31 23:59:00'), relative offset (FAKETIME='-1d'), advance-rate (FAKETIME='@2026-12-31 23:59:00 x5' for 5x speed), per-process scope via LD_PRELOAD, and FAKETIME_NO_CACHE for high-resolution mocking. Use when you need to fake time, mock the clock, or freeze time for C/C++ or any native binary that needs deterministic wall-clock time.

mockclock-jvm

Wraps Java's java.time.Clock + InstantSource dependency-injection pattern for testing time-sensitive code. Covers Clock.fixed(instant, zone), Clock.offset(baseClock, duration), Clock.systemDefaultZone() for production, the InstantSource interface (Java 17+), and the recommended dependency-injection pattern (constructor-inject Clock instead of calling Instant.now() directly). Use when you need to wire the clock-injection pattern (Clock.fixed, Clock.offset, MutableClock, InstantSource, Spring @Bean) into JVM (Java / Kotlin / Scala) production or test code. For pure DST transition reference (skipped or repeated hours, IANA DB, cron-double-fire bug classes) without a clock-injection need, use dst-transition-reference instead.

sinon-fake-timers-js

Wraps Sinon's standalone @sinonjs/fake-timers library for JS/TS testing: install(), tick() / tickAsync(), setSystemTime(), restore(); covers timers (setTimeout / setInterval / requestAnimationFrame), Date / performance.now() / hrtime, and the toFake option for selective override. Runner-agnostic - drives the clock directly in Mocha, AVA, Jasmine, node:test, or the browser. Use when JS/TS code needs deterministic timer or clock control and the test runner does not already expose this library behind its own built-in fake-timer API.

timecop-ruby

Wraps timecop (github.com/travisjeffery/timecop), the Ruby time-mocking gem: Timecop.freeze, Timecop.travel, Timecop.scale (time-speedup), Timecop.return (cleanup), and RSpec-friendly helpers. Use when testing Ruby/Rails code that calls Time / Date / DateTime.

timezone-test-matrix-builder

Builds a timezone, daylight saving time (DST), and leap year / leap second test matrix from wherever a codebase reads or formats dates and times. Finds time-handling code (grep for datetime / Date / Instant / time.time / timezone), sorts each spot into storage, business-logic, display, cron, or billing, picks the edge cases that matter (DST spring-forward / fall-back, ambiguous local time, leap day Feb 29, ISO 8601 / RFC 3339 round-trip, zone-database updates), and emits per-spot test stubs wired to the language's fake-clock (mock-time) library. Use when a codebase needs timezone, DST, and leap-year test coverage derived from its own date/time usage.