CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/fake-clock-testing

Fake clocks / freeze time in tests across every mainstream runtime: freezegun (Python), Jest fake timers + Sinon @sinonjs/fake-timers (JS/TS), timecop (Ruby), java.time.Clock / InstantSource injection (JVM), .NET TimeProvider / FakeTimeProvider, and libfaketime (LD_PRELOAD for any native binary). Covers the language-agnostic discipline - inject or patch the clock, freeze vs tick vs advance vs set-system-time semantics, teardown so fake clocks never leak between tests - plus the shared anti-pattern table (real sleep under a frozen clock, leaked clock state, timezone-dependent assertions). Per-library setup, API, and CI recipes live in references/{python,js,ruby,jvm,dotnet,libfaketime}.md. Use when tests need deterministic control of now(), timers, or timeouts in any language, or when choosing the right fake-clock tool for a stack.

93

Quality

93%

Does it follow best practices?

Impact

Average score across 3 eval scenarios

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

dotnet.mdreferences/

.NET - TimeProvider and FakeTimeProvider

.NET 8 introduced System.TimeProvider, the testable time abstraction: the production singleton TimeProvider.System wraps DateTimeOffset.UtcNow, the local TimeZoneInfo, Stopwatch timestamps, and System.Threading.Timer. Tests use FakeTimeProvider (namespace Microsoft.Extensions.Time.Testing) which subclasses TimeProvider.

Install (test projects only)

<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.*" />

TimeProvider itself is in the .NET 8+ runtime; no production package.

Inject TimeProvider

public class TokenService
{
    private readonly TimeProvider _time;
    public TokenService(TimeProvider time) => _time = time;
    public bool IsExpired(DateTimeOffset expiresAt) => _time.GetUtcNow() > expiresAt;
}

services.AddSingleton(TimeProvider.System);   // production DI

Register FakeTimeProvider, never TimeProvider.System, in test DI.

Freeze and advance

var fakeTime = new FakeTimeProvider(
    new DateTimeOffset(2026, 5, 20, 12, 0, 0, TimeSpan.Zero));   // frozen start
var svc = new TokenService(fakeTime);
var expiresAt = fakeTime.GetUtcNow().AddHours(1);

fakeTime.Advance(TimeSpan.FromHours(2));      // move forward
Assert.True(svc.IsExpired(expiresAt));

SetUtcNow(DateTimeOffset) repositions the clock; the value must not be earlier than the current fake time (throws ArgumentOutOfRangeException) - the clock cannot go backwards.

Auto-advance on every read

Per FakeTimeProvider.AutoAdvanceAmount: "the amount of time by which time advances whenever the clock is read."

fakeTime.AutoAdvanceAmount = TimeSpan.FromMilliseconds(100);
var t1 = fakeTime.GetUtcNow();
var t2 = fakeTime.GetUtcNow();
Assert.Equal(TimeSpan.FromMilliseconds(100), t2 - t1);

Prefer explicit Advance for tests needing exact instants.

Task.Delay and timers on the virtual clock

Delay(TimeProvider, TimeSpan, CancellationToken) is an extension in TimeProviderTaskExtensions; CreateTimer callbacks fire only when Advance passes the due time:

var delayTask = fakeTime.Delay(TimeSpan.FromSeconds(30));
var fired = false;
_ = 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);

If a test hangs, the code under test is calling real Task.Delay(int) or Thread.Sleep instead of the injected provider - fix the injection.

Local time zone testing

fakeTime.SetUtcNow(new DateTimeOffset(2026, 3, 8, 7, 0, 0, TimeSpan.Zero));
fakeTime.SetLocalTimeZone(TimeZoneInfo.FindSystemTimeZoneById("America/New_York"));
DateTimeOffset local = fakeTime.GetLocalNow();   // UTC-5 or UTC-4 depending on DST

Per TimeProvider.GetLocalNow, GetLocalNow() converts the UTC instant to the provider's LocalTimeZone - no environment variables or system clock changes needed.

Migrating from ISystemClock (pre-.NET 8)

The Microsoft.Extensions stack previously used ISystemClock (Microsoft.Extensions.Internal, a single UtcNow property, marked "not intended to be used directly from your code"). Migration: replace ISystemClock injection with TimeProvider and hand-rolled fakes with FakeTimeProvider - TimeProvider also covers timers and high-frequency timestamps, making it the complete replacement.

Anti-patterns

Anti-patternWhy it failsFix
DateTime.UtcNow / DateTimeOffset.UtcNow in codeNot injectableInject TimeProvider; _time.GetUtcNow()
Static DateTime mocks via Fakes/HarmonyIL rewriting, special runnersDI with TimeProvider
SetUtcNow earlier than currentThrows ArgumentOutOfRangeExceptionAdvance, or a fresh FakeTimeProvider
No await Task.Yield() after AdvanceContinuations haven't run yetYield or await the completed task
AutoAdvanceAmount in exact-instant testsClock shifts between readsKeep the default TimeSpan.Zero
TimeProvider.System in test DIWall-clock flakeRegister FakeTimeProvider

Limitations

  • Task.Delay(int) overloads without a TimeProvider still use wall-clock time; always use the timeProvider.Delay(TimeSpan) form.
  • Thread.Sleep is not controlled; restructure to await timeProvider.Delay(...).
  • GetTimestamp() values derive from the fake UTC instant, not Stopwatch (per TimestampFrequency).
  • Third-party libraries calling DateTime.UtcNow internally are unaffected.

References

SKILL.md

tile.json