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.
Install with skills.sh (any agent)
npx skills add testland/qa --skill fake-clock-testingfake-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:
| Family | How it works | Libraries |
|---|---|---|
| Injection | Production code takes a clock dependency; tests pass a fake | java.time.Clock / InstantSource (JVM), TimeProvider / FakeTimeProvider (.NET) |
| Patching | The library rewrites the runtime's time APIs in test scope | freezegun (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
| Stack | Tool | Reference |
|---|---|---|
| Python (pytest / unittest) | freezegun | references/python.md |
| JS/TS in Jest | jest.useFakeTimers() (wraps Sinon's engine) | references/js.md |
| JS/TS in Mocha / Vitest / AVA / node:test / browser | @sinonjs/fake-timers directly | references/js.md |
| Ruby / Rails | timecop | references/ruby.md |
| Java / Kotlin / Scala | Clock.fixed / MutableClock / InstantSource injection | references/jvm.md |
| C# / F# (.NET 8+) | TimeProvider + FakeTimeProvider | references/dotnet.md |
| C/C++, closed-source or multi-process binaries | libfaketime (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:
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-pattern | Why it fails | Fix |
|---|---|---|
Real sleep() inside a frozen-clock test | Sleep is wall-clock; the frozen clock never advances - the test just gets slower | Advance the fake clock instead (tick / Advance / advanceTimersByTime) |
| Fake clock leaking between tests | Restore skipped on assertion failure; later tests inherit frozen time and fail mysteriously | Restore in afterEach / fixture teardown, not the test body |
| Timezone-dependent assertions | new Date().toString() / datetime.now() render in host-local zone; green locally, red in CI | Assert on UTC instants or set the zone explicitly (TZ env, tz=, SetLocalTimeZone) |
| Mixing real and fake time in one test | Real fetch / Thread.Sleep / C-extension resolves on the real clock; races with faked timers | Fake 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 boundaries | Freeze a full ISO-8601 instant with offset |
Hardcoded timestamps that age (assert year == 2026) | Test rots on the next New Year | Derive expectations from the frozen instant |
| Advancing years via timer ticks | Every intermediate timer fires; test crawls | Set / jump to the target instant instead |
| Asserting durations from the wall clock | Frozen wall clock breaks elapsed-time math | Use the monotonic clock for durations; fake it only when the library supports it |
Limitations
References
.NET - TimeProvider and FakeTimeProvider
View source (opens in new window).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 DIRegister 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 (opens in new window): "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 (opens in new window); 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 DSTPer TimeProvider.GetLocalNow (opens in new window), 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 (opens in new window) (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-pattern | Why it fails | Fix |
|---|---|---|
DateTime.UtcNow / DateTimeOffset.UtcNow in code | Not injectable | Inject TimeProvider; _time.GetUtcNow() |
Static DateTime mocks via Fakes/Harmony | IL rewriting, special runners | DI with TimeProvider |
SetUtcNow earlier than current | Throws ArgumentOutOfRangeException | Advance, or a fresh FakeTimeProvider |
No await Task.Yield() after Advance | Continuations haven't run yet | Yield or await the completed task |
AutoAdvanceAmount in exact-instant tests | Clock shifts between reads | Keep the default TimeSpan.Zero |
TimeProvider.System in test DI | Wall-clock flake | Register FakeTimeProvider |
Limitations
References
JavaScript / TypeScript - Jest fake timers and Sinon @sinonjs/fake-timers
View source (opens in new window)JavaScript / TypeScript - Jest fake timers and Sinon @sinonjs/fake-timers
Both share one engine: per jestjs.io/docs/timer-mocks (opens in new window), Jest 27+ uses modern fake timers built on @sinonjs/fake-timers (opens in new window). Use Jest's wrapper inside Jest; use the Sinon library directly in Mocha, Vitest, Jasmine, AVA, node:test, or the browser. The API differs only in naming: jest.advanceTimersByTime vs clock.tick, jest.setSystemTime vs clock.setSystemTime.
Jest - enable, advance, restore
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-05-20T14:30:00Z'));
});
afterAll(() => jest.useRealTimers());
test('debounce fires after 300ms', () => {
let fired = false;
setTimeout(() => { fired = true; }, 300);
jest.advanceTimersByTime(299);
expect(fired).toBe(false);
jest.advanceTimersByTime(1);
expect(fired).toBe(true);
});Async chains need the Async variant so microtasks drain between ticks:
await jest.advanceTimersByTimeAsync(100);jest.runAllTimers() drains every pending timer (recursion included); jest.runOnlyPendingTimers() runs the currently-queued set only - use it for self-rescheduling code to avoid infinite loops.
Sinon fake-timers - install, tick, restore
import FakeTimers from '@sinonjs/fake-timers'; // npm i -D @sinonjs/fake-timers
const clock = FakeTimers.install({ now: new Date('2026-05-20T14:30:00Z').getTime() });
clock.tick(1000); // sync advance
await clock.tickAsync(300); // advance + drain microtasks
clock.setSystemTime(new Date('2027-01-01T00:00:00Z')); // jump, no timers fire
clock.uninstall(); // ALWAYS in afterEachSelective faking
Keep real performance.now() / nextTick while faking timers and Date:
// Jest
jest.useFakeTimers({ doNotFake: ['nextTick', 'queueMicrotask'],
now: new Date('2026-05-20T14:30:00Z').getTime() });
// Sinon
const clock = FakeTimers.install({ toFake: ['setTimeout', 'setInterval', 'Date'] });DST tests
Both fake UTC time; for local-zone DST behaviour set the runtime zone first, then position the clock at the transition's UTC instant:
process.env.TZ = 'America/New_York';
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-03-08T06:30:00Z')); // 02:30 local - non-existent
expect(new Date().toString()).toMatch(/03:30/); // Node normalisesReset process.env.TZ per test - it is process-global.
Fake timers + mocked fetch
A real fetch resolves on the real clock and races faked timers - mock it and await an async advance:
test('debounce + fetch', async () => {
global.fetch = jest.fn().mockResolvedValue({ json: () => ({ ok: true }) });
myDebouncedFetch();
await jest.advanceTimersByTimeAsync(300);
expect(fetch).toHaveBeenCalled();
});Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
jest.useFakeTimers('legacy') | Deprecated; doesn't fake Date | Modern is the default since Jest 27 |
Forget useRealTimers / clock.uninstall | Later tests inherit the fake clock | afterEach hook |
Sync tick / advanceTimersByTime for promise chains | Microtasks don't drain | tickAsync / advanceTimersByTimeAsync |
Skip setSystemTime, then read Date | Date.now() returns real time | Always position the clock |
tick(86400 * 365 * 1000) to "advance a year" | Every timer fires one-by-one; crawls | setSystemTime jump |
DST test without process.env.TZ | UTC-only; the local-zone branch never runs | Set TZ explicitly per test |
Limitations
References
JVM - java.time.Clock and InstantSource injection
View source (opens in new window)JVM - java.time.Clock and InstantSource injection
The JVM has no "freeze clock" library because java.time (Java 8+) was designed with dependency-injected Clock as the testing pattern. Per docs.oracle.com Clock (opens in new window): "Most application code should inject a Clock into any method that needs the current instant and date/time." Production injects Clock.systemDefaultZone(); tests inject Clock.fixed(...). No global monkey-patching.
The injection pattern
public class TaskScheduler {
private final Clock clock;
public TaskScheduler(Clock clock) { this.clock = clock; }
public Task scheduleNext(Duration after) {
return new Task(Instant.now(clock).plus(after));
}
}
// production wiring
TaskScheduler prod = new TaskScheduler(Clock.systemDefaultZone());Clock.fixed (frozen)
@Test
void scheduleNext() {
Clock fixed = Clock.fixed(Instant.parse("2026-05-20T14:30:00Z"),
ZoneId.of("America/New_York"));
Task task = new TaskScheduler(fixed).scheduleNext(Duration.ofMinutes(5));
assertEquals(Instant.parse("2026-05-20T14:35:00Z"), task.getScheduledAt());
}Clock.fixed never advances - successive Instant.now(fixed) calls return the same value.
Clock.offset (relative) and a mutable test clock
Clock realPlus10 = Clock.offset(Clock.systemDefaultZone(), Duration.ofMinutes(10));For advance-mid-test semantics, a small custom clock:
public class MutableClock extends Clock {
private Instant instant;
private final ZoneId zone;
public MutableClock(Instant instant, ZoneId zone) { this.instant = instant; this.zone = zone; }
public void setInstant(Instant i) { this.instant = i; }
public void advance(Duration d) { instant = instant.plus(d); }
@Override public Clock withZone(ZoneId z) { return new MutableClock(instant, z); }
@Override public ZoneId getZone() { return zone; }
@Override public Instant instant() { return instant; }
}InstantSource (Java 17+)
Per docs.oracle.com InstantSource (opens in new window), a narrower interface than Clock (just instant(), no zone) - prefer it when code only needs the instant; a lambda is a complete fake:
InstantSource fake = () -> Instant.parse("2026-05-20T14:30:00Z");Spring DI integration
@Configuration
public class ClockConfig {
@Bean public Clock clock() { return Clock.systemDefaultZone(); }
}
@TestConfiguration
public class TestClockConfig {
@Bean public Clock clock() {
return Clock.fixed(Instant.parse("2026-05-20T14:30:00Z"), ZoneOffset.UTC);
}
}DST tests
Clock fixed = Clock.fixed(Instant.parse("2026-03-08T07:30:00Z"), // 02:30 local - non-existent
ZoneId.of("America/New_York"));
ZonedDateTime zdt = ZonedDateTime.ofInstant(fixed.instant(), fixed.getZone());
// assert per the resolver rules documented in dst-transition-referenceAnti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Instant.now() / System.currentTimeMillis() directly | Not injectable | Inject Clock; Instant.now(clock) |
| Static-mocking Clock with PowerMock | Brittle bytecode rewriting | Use DI |
No zone in Clock.fixed | Defaults matter; local-time tests degenerate | Always pass the zone |
| Only frozen clocks, never advancing | Duration arithmetic untested | MutableClock.advance |
| Multiple Clocks per service | Coordination bugs | One Clock per service |
Limitations
References
libfaketime - the LD_PRELOAD escape hatch
View source (opens in new window)libfaketime - the LD_PRELOAD escape hatch
Per github.com/wolfcw/libfaketime (opens in new window), libfaketime returns a value derived from the FAKETIME environment variable instead of the real clock by intercepting libc time() / gettimeofday() / clock_gettime(). Because it hooks libc, it works for any dynamically linked binary - C/C++, Go (cgo builds), Rust, Python - including processes you don't control the source of. Reach for it when language-native fakes cannot patch the code (C extensions, closed-source binaries, multi-process integration tests).
Install
sudo apt install faketime # Debian/Ubuntu
brew install libfaketime # macOS
# or from source: git clone https://github.com/wolfcw/libfaketime && make && sudo make installAbsolute-date mode
faketime '2026-12-31 23:59:00' your_command
# equivalent raw form:
LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1 \
FAKETIME='2026-12-31 23:59:00' your_commandRelative offset and advance-rate modes
faketime '-1d' your_command # 1 day in the past
faketime '+2h30m' your_command # 2h30m ahead
faketime -f '@2026-12-31 23:59:00 x10' your_cmd # start there, run at 10x speedThe x<rate> spec suits scheduler/cron simulations - e.g. faketime -f '@2026-01-01 00:00:00 x5256' ./cron-runner simulates a year in ~10 minutes.
High-resolution mode
FAKETIME_NO_CACHE=1 faketime '2026-12-31 23:59:00' your_commandDisables libfaketime's per-second caching so code reading time hundreds of times per second sees consistent values.
Asserting from a test runner
libfaketime emits nothing itself - assert on the wrapped program's visible behaviour:
import subprocess
def test_cron_fires_at_midnight():
result = subprocess.run(
["faketime", "2026-12-31 23:59:30", "./cron-runner"],
capture_output=True, text=True, timeout=5,
)
assert "Fired at 2027-01-01 00:00:00" in result.stdoutDST recipe - non-existent local time
TZ='America/New_York' faketime '2026-03-08 02:30:00' ./my-programUS Eastern springs forward at 02:00 on 2026-03-08, so 02:30 local does not exist; TZ makes the program resolve the faked instant in Eastern. Assert the program skips the job or normalises to 03:30, per dst-transition-reference.
CI integration
jobs:
time-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: sudo apt-get install -y faketime
- run: pytest tests/time/Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Statically linked binaries | LD_PRELOAD has no symbols to intercept | Language-native fake clock |
Raw LD_PRELOAD with a wrong path | Silently no-ops | Use the faketime wrapper |
Spring-forward test without TZ | Fake time resolves in UTC only | Prefix TZ='<zone>' |
Missing FAKETIME_NO_CACHE=1 for fast-polling code | Time stalls between cache refreshes | Set it explicitly |
| Using it against the JVM | Some JVM time calls bypass libc | Clock injection (jvm.md (opens in new window)) |
Limitations
References
Python - freezegun
View source (opens in new window)Python - freezegun
freezegun patches datetime.datetime, datetime.date, time.time, time.gmtime, time.localtime, time.strftime, and asyncio time across the test scope. Per github.com/spulec/freezegun (opens in new window).
Install
pip install freezegunDecorator (most common)
from freezegun import freeze_time
from datetime import datetime
@freeze_time("2026-05-20T14:30:00")
def test_today_is_may_20():
assert datetime.now().strftime("%Y-%m-%d") == "2026-05-20"Context manager and manual start/stop
with freeze_time("2026-05-20T14:30:00"):
assert datetime.now().strftime("%Y-%m-%d") == "2026-05-20"
freezer = freeze_time("2026-05-20T14:30:00")
freezer.start()
try:
...
finally:
freezer.stop()Tick mode and moving mid-test
@freeze_time("2026-05-20T14:30:00", tick=True) # real time passes from the frozen start
def test_clock_advances():
t1 = datetime.now()
t2 = datetime.now()
assert t2 > t1
@freeze_time("2026-05-20T14:30:00")
def test_advance_one_day(freezer):
freezer.move_to("2026-05-21T14:30:00") # or freezer.tick(delta=timedelta(hours=24))
assert datetime.now().day == 21Timezone offset
@freeze_time("2026-05-20T14:30:00", tz_offset=-5)
def test_eastern_time():
assert datetime.utcnow().hour == 19 # 14:30 + 5
assert datetime.now().hour == 14 # wall clocktz_offset is a fixed offset and does not know about DST. For local-zone behaviour at a transition, freeze UTC and read through zoneinfo:
from zoneinfo import ZoneInfo
@freeze_time("2026-03-08T07:30:00") # 02:30 EST or 03:30 EDT depending on resolution
def test_spring_forward_handling():
ny = datetime.now(ZoneInfo("America/New_York"))
# assert against the documented library behaviour per dst-transition-referenceAsync support
@freeze_time("2026-05-20T14:30:00")
async def test_async_now():
await asyncio.sleep(0)
assert datetime.now().strftime("%Y") == "2026"Per freezegun docs: "freezegun is compatible with asyncio."
CI integration
jobs:
python-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
- run: pip install -e ".[test]" freezegun
- run: pytest tests/Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
freeze_time("2026-05-20") (date only) | Interpreted as midnight local; subtle | Use a full ISO datetime |
time.sleep(...) inside a frozen block | Sleep is real-time; frozen clock doesn't advance | Use freezer.tick() |
Mock datetime.utcnow separately | Conflicts with freezegun | Let freezegun patch both |
| Forget freezer cleanup in fixtures | Cross-test contamination | Use decorator or with |
DST test without tz_offset or zoneinfo | Result is UTC; misses local behaviour | Combine with zoneinfo |
@freeze_time on a class without decorate_class=True | Methods not patched | Use the class decorator explicitly |
Limitations
References
Ruby - timecop
View source (opens in new window)Ruby - timecop
timecop is the canonical Ruby time-mocking gem. Per github.com/travisjeffery/timecop (opens in new window), it patches Time.now, Date.today, DateTime.now, and Time.new.
Install
# Gemfile
group :test do
gem 'timecop'
endTimecop.freeze (snapshot) vs Timecop.travel (clock continues)
Timecop.freeze(Time.local(2026, 5, 20, 14, 30)) do
expect(Time.now.strftime('%Y-%m-%d')).to eq('2026-05-20')
end # auto-restored after the block
Timecop.travel(Time.local(2026, 12, 31, 23, 59, 0)) do
sleep 5 # real sleep; travel keeps the clock ticking from the offset
expect(Time.now).to be_within(6.seconds).of(Time.local(2026, 12, 31, 23, 59, 5))
endfreeze pauses the clock; travel offsets it and lets it keep ticking. They are not interchangeable - use freeze when the clock must not advance.
Manual control and cleanup
Timecop.freeze(Time.local(2026, 5, 20, 14, 30))
# ... test code
Timecop.return # restore; wrap in ensure when not using the block formRSpec safety net:
RSpec.configure do |config|
config.after(:each) { Timecop.return }
endTimecop.scale (time speed-up)
Timecop.scale(3600) do # 1 real second = 1 simulated hour
start = Time.now
sleep 1
expect(Time.now - start).to be_within(60).of(3600)
endDST tests (Rails / ActiveSupport)
Ruby Time doesn't track zones natively; use ActiveSupport's Time.zone:
require 'active_support/time'
Time.zone = 'America/New_York'
Timecop.freeze(Time.zone.local(2026, 3, 8, 2, 30, 0)) do
# 02:30 local doesn't exist on this spring-forward date;
# assert the documented behaviour per dst-transition-reference
endSave and restore Time.zone per test - it is process-global config.
Rails controller example
RSpec.describe BookingController do
it 'rejects past dates' do
Timecop.freeze(Date.new(2026, 5, 20)) do
post :create, params: { date: '2026-05-19' }
expect(response.status).to eq(400)
end
end
endAnti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Forget Timecop.return | Cross-test contamination | RSpec after-each hook |
freeze + sleep | Sleep is real-time; the frozen clock stays put | Use travel or scale |
Hardcode Time.zone in tests | Config bleeds across tests | Save/restore the zone per test |
| DST test without ActiveSupport zone | Ruby Time has no zone tracking | Time.zone + Time.zone.local |
Date.today without a freeze | Test fails at midnight | Always freeze |
Limitations
References
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, refreshable per-region test-data fixtures, and the leap-second reference (23:59:60 insertion, time_t stalls, leap-smear vs step, monotonic-clock fixes) live in references/. Use when designing or auditing time-handling code or test cases, or when auditing leap-second assumptions.
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.