Testing time-dependent code

Injectable clocks instead of global stubs, freezing and travelling time in tests, deterministic inputs, fake timers for scheduled work, and testing across zone boundaries.

Make the clock a dependency

Code that calls datetime.now() directly cannot be tested without patching a global. Accepting a clock as a parameter or field makes the dependency visible and the test trivial.

from datetime import datetime, timedelta, timezone

class Clock:
    def now(self):
        return datetime.now(timezone.utc)

class FakeClock:
    def __init__(self, fixed):
        self.fixed = fixed
    def now(self):
        return self.fixed

class Subscription:
    def __init__(self, clock, days=30):
        self.clock = clock
        self.days = days

    def expires_at(self, start):
        return start + timedelta(days=self.days)

    def is_expired(self, start):
        return self.clock.now() >= self.expires_at(start)

def test_expiry_boundary():
    start = datetime(2026, 9, 1, tzinfo=timezone.utc)
    just_before = FakeClock(datetime(2026, 9, 30, 23, 59, 59, tzinfo=timezone.utc))
    at_boundary = FakeClock(datetime(2026, 10, 1, tzinfo=timezone.utc))
    assert not Subscription(just_before).is_expired(start)
    assert Subscription(at_boundary).is_expired(start)

Freezing and travelling

# freezegun patches the clock; useful for code you cannot refactor
from freezegun import freeze_time

@freeze_time("2026-09-18T10:30:00Z")
def test_daily_report_uses_today():
    assert report_date() == date(2026, 9, 18)

# travel forward to test a scheduled transition
with freeze_time("2026-09-18T10:30:00Z") as frozen:
    frozen.tick(delta=timedelta(hours=2))   # explicit advance, not real waiting
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-09-18T10:30:00Z")); });
afterEach(() => { vi.useRealTimers(); });

it("retries with backoff without waiting", () => {
  const fn = vi.fn();
  scheduleRetry(fn, { attempts: 3, backoffMs: 1000 });
  vi.advanceTimersByTime(0);
  vi.advanceTimersByTime(1000);
  vi.advanceTimersByTime(2000);
  expect(fn).toHaveBeenCalledTimes(3);
});

Fake timers make a test that would take minutes of real waiting finish instantly. They also expose code that depends on the real clock, because timers never fire on their own.

Testing across zones and boundaries

TestWhat to freezeWhat to assert
Day boundary23:59:59 and 00:00:00 UTCThe record lands in the correct day bucket
Spring-forward gapA local time inside the gapThe code rejects or adjusts, never silently shifts
Fall-back repeatAn ambiguous local timeThe chosen offset matches the documented rule
Leap day29 February 2028Monthly and yearly arithmetic clamps correctly
Leap second23:59:60 handlingElapsed time does not go negative
Zone changeA date before and after a tz rule changeThe offset comes from the loaded database, not a constant
# run the whole suite under several zones in CI
for tz in UTC Europe/Paris America/New_York Australia/Lord_Howe Asia/Kathmandu; do
  TZ=$tz pytest -q || exit 1
done
⚠️
A test suite that passes only in UTC is not testing time zones. Run at least one full pass in a zone with a non-hour offset such as Asia/Kathmandu (+05:45) and one southern-hemisphere zone, where daylight saving moves in the opposite direction.

FAQ

Should I patch the system clock in tests?
Prefer injecting a clock. Patching globals couples tests to implementation details and can leak into other tests when a patch is not undone. Use a patcher only for third-party code you cannot change.
How do I test code that must not use the wall clock?
Lint for direct calls to the wall-clock API. A rule forbidding Date.now() or datetime.now() outside a clock module removes the whole category of bug.

Monotonic clocks and elapsed time Daylight saving and its edge cases

Last refreshed 2026-09-18.