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 waitingimport { 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
| Test | What to freeze | What to assert |
|---|---|---|
| Day boundary | 23:59:59 and 00:00:00 UTC | The record lands in the correct day bucket |
| Spring-forward gap | A local time inside the gap | The code rejects or adjusts, never silently shifts |
| Fall-back repeat | An ambiguous local time | The chosen offset matches the documented rule |
| Leap day | 29 February 2028 | Monthly and yearly arithmetic clamps correctly |
| Leap second | 23:59:60 handling | Elapsed time does not go negative |
| Zone change | A date before and after a tz rule change | The 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.Related
Monotonic clocks and elapsed time Daylight saving and its edge cases
Last refreshed 2026-09-18.