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
name:
fake-clock-testing
description:
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.

fake-clock-testing

Overview

Tests that read the real clock flake at midnight, on DST transitions, and on slow CI runners. The fix is always the same discipline, whatever the language: replace the clock the code under test reads, drive it explicitly, and restore it afterwards. Two mechanism families exist:

FamilyHow it worksLibraries
InjectionProduction code takes a clock dependency; tests pass a fakejava.time.Clock / InstantSource (JVM), TimeProvider / FakeTimeProvider (.NET)
PatchingThe library rewrites the runtime's time APIs in test scopefreezegun (Python), Jest fake timers + Sinon @sinonjs/fake-timers (JS), timecop (Ruby), libfaketime (libc interception, any binary)

Injection needs source control of the code under test but has no global state; patching works on unmodified code but must be scoped and torn down per test.

Choosing the tool

StackToolReference
Python (pytest / unittest)freezegunreferences/python.md
JS/TS in Jestjest.useFakeTimers() (wraps Sinon's engine)references/js.md
JS/TS in Mocha / Vitest / AVA / node:test / browser@sinonjs/fake-timers directlyreferences/js.md
Ruby / Railstimecopreferences/ruby.md
Java / Kotlin / ScalaClock.fixed / MutableClock / InstantSource injectionreferences/jvm.md
C# / F# (.NET 8+)TimeProvider + FakeTimeProviderreferences/dotnet.md
C/C++, closed-source or multi-process binarieslibfaketime (LD_PRELOAD escape hatch)references/libfaketime.md

libfaketime is the fallback when language-native fakes cannot reach the code: it intercepts libc time() / gettimeofday() / clock_gettime(), so it covers any dynamically linked binary regardless of language.

The four clock operations

Every library exposes some subset of the same four operations; tests should name which one they rely on:

  1. Freeze - pin now() to a fixed instant; successive reads are equal (freeze_time, Timecop.freeze, Clock.fixed, jest.setSystemTime after useFakeTimers).
  2. Advance - move the frozen clock forward by a duration, firing any timers that come due (jest.advanceTimersByTime, clock.tick, FakeTimeProvider.Advance, freezer.tick).
  3. Set / jump - reposition the clock to an absolute instant without firing intermediate timers (setSystemTime, freezer.move_to, Timecop.travel). Use for large jumps; advancing through a year fires every intermediate timer one-by-one and crawls.
  4. Restore - put the real clock back (useRealTimers, clock.uninstall, Timecop.return, decorator/context-manager exit). Always in an after-each hook, never at the end of the test body - a failed assertion would skip it and leak the fake clock into the next test.

Worked example - a boundary test on an expiring token

The canonical shape, here with .NET's FakeTimeProvider (the same freeze-then-advance pattern maps 1:1 onto every library in references/):

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

Assert.False(svc.IsExpired(expiresAt));            // frozen: still valid

fakeTime.Advance(TimeSpan.FromHours(1));           // advance to the boundary
Assert.False(svc.IsExpired(expiresAt));            // boundary is inclusive

fakeTime.Advance(TimeSpan.FromTicks(1));           // one tick past
Assert.True(svc.IsExpired(expiresAt));

The test asserts on both sides of the boundary and never sleeps; it passes in microseconds on any runner at any wall-clock time. In freezegun the same test is freeze_time(...) + freezer.tick(...); in Jest, setSystemTime + advanceTimersByTime; in Ruby, Timecop.freeze + a second freeze at the boundary.

Anti-patterns

Anti-patternWhy it failsFix
Real sleep() inside a frozen-clock testSleep is wall-clock; the frozen clock never advances - the test just gets slowerAdvance the fake clock instead (tick / Advance / advanceTimersByTime)
Fake clock leaking between testsRestore skipped on assertion failure; later tests inherit frozen time and fail mysteriouslyRestore in afterEach / fixture teardown, not the test body
Timezone-dependent assertionsnew Date().toString() / datetime.now() render in host-local zone; green locally, red in CIAssert on UTC instants or set the zone explicitly (TZ env, tz=, SetLocalTimeZone)
Mixing real and fake time in one testReal fetch / Thread.Sleep / C-extension resolves on the real clock; races with faked timersFake everything time-related in the test, or fake nothing
Date-only freeze (freeze_time("2026-05-20"))Interpreted as midnight local; off-by-one around zone boundariesFreeze a full ISO-8601 instant with offset
Hardcoded timestamps that age (assert year == 2026)Test rots on the next New YearDerive expectations from the frozen instant
Advancing years via timer ticksEvery intermediate timer fires; test crawlsSet / jump to the target instant instead
Asserting durations from the wall clockFrozen wall clock breaks elapsed-time mathUse the monotonic clock for durations; fake it only when the library supports it

Limitations

  • Patching libraries stop at the language boundary. C extensions, native gems, and statically linked binaries read the real clock_gettime(); use libfaketime for those (references/libfaketime.md).
  • Injection requires owning the code. Third-party libraries that call Instant.now() / DateTime.UtcNow internally cannot be reached by injected clocks.
  • Monotonic clocks are usually not faked by default (performance.now, process.hrtime, GetTimestamp); check each library's selective-faking option before asserting on them.
  • DST resolution depends on the runtime's tz database (ICU in Node, system tzdata in Python, JDK tzdata on the JVM). Pin the zone per test and assert against dst-transition-reference's documented behaviours.
  • No library simulates leap seconds - see dst-transition-reference references/leap-seconds.md.

References

Workspace
testland
Visibility
Public
Created
Last updated
Publish Source
GitHub
Badge
testland/fake-clock-testing badge