Testing Time: The Clock Pattern for Deterministic Tests

Testing Time: The Clock Pattern for Deterministic Tests

December 6, 2025

Fast, deterministic tests are foundational to high-performing teams. When your test suite runs in seconds, developers get immediate feedback and can iterate quickly. When tests produce the same result every time, you eliminate flaky failures and build confidence. But there’s a class of functionality that seems inherently difficult to test this way: anything involving time.

I often run into the problem where my team needs to test the assignment of time. This can be anything from checking a database had the correct created_at date inserted, to testing cache timeouts, or even the dreaded locale-related timezones. The problem stems from using a language’s standard library to allocate time in your codebase.

For example, let’s create a simple cache timeout function:

from datetime import datetime, UTC, timedelta


def is_timed_out(created_at: datetime) -> bool:
    return datetime.now(UTC) >= created_at + timedelta(minutes=10)

This makes testing the code that allocates the ’now’ time difficult. Without any changes, the test would take at least 10 minutes to run:

def test_is_timed_out():
    created_at = datetime.now(UTC)
    assert is_timed_out(created_at) is False

    # wait for 10 minutes - what a slow test!
    sleep(10 * 60)

    assert is_timed_out(created_at) is True

How do we solve this problem?

There are a number of approaches we could take, but I’ll outline what has worked for me across numerous languages, and given me complete control over time within my tests.

The key is to abstract the use of the standard libraries NOW function. Hardcoding this inside your production code, means you cannot control what date is allocated, and hence cannot deterministically test the code that calls it.

Clocks!

By following the Clock pattern laid out in the Growing Object Oriented Software Guided By Tests book - this abstracts NOW function behind a clock interface and enables the control we need. It’s easier to show how this works in an example:

class Clock(Protocol):
    def now(self) -> datetime:
        pass


class UTCClock(Clock):
    def now(self) -> datetime:
        return datetime.now(UTC)


UTC_CLOCK = UTCClock()

Let’s change our function to use a clock, defaulting to using a UTC_CLOCK to preserve behaviour:

def is_timed_out(created_at: datetime, clock: Clock = UTC_CLOCK) -> bool:
    return clock.now() >= created_at + timedelta(minutes=10)

Why the PROTOCOL??

This allows us to re-implement the clock for testing - allowing us to jump to the future without having to wait around for the future to happen. Here’s how:

class ControlledClock(Clock):
    def __init__(self):
        self.current_time = datetime.now(UTC)

    def tick_forward(self, delta: timedelta) -> None:
        self.current_time = self.current_time + delta

    def now(self) -> datetime:
        return self.current_time

We can use this in our test to deterministically check a date is timed out after 10 minutes:

def test_is_timed_out():
    clock = ControlledClock()
    created_at = clock.now()

    # time has not moved forward, so clock.now() and created_at are still the same
    assert is_timed_out(created_at, clock) is False

    # move time into the future
    clock.tick_forward(timedelta(minutes=10))

    # the clock is now set to 10 minutes later and the created_at date is timed out
    assert is_timed_out(created_at, clock) is True

Why Not Just Mock datetime.now?

You might be thinking: “Can’t I just mock datetime.now() in my tests?” Yes, you technically can - and many developers do. But there are several reasons why dependency injection with a Clock is superior to monkey-patching:

Explicitness over magic

When a function accepts a clock parameter, it’s immediately clear that it depends on time. This makes the code easier to understand and reason about. Mocking datetime.now() globally creates invisible dependencies that future maintainers won’t discover until they read the tests.

Test isolation

Monkey-patching global state can lead to test pollution. If one test mocks datetime.now() and doesn’t clean up properly, subsequent tests might fail mysteriously. Each test using a ControlledClock gets its own isolated instance.

No magic imports or decorators

Libraries like freezegun or unittest.mock require special decorators or context managers. Your tests become cluttered with @freeze_time or with patch(‘datetime.datetime’). The Clock pattern keeps tests clean and readable.

Works everywhere

The Clock abstraction works identically in production and test code. You can even use ControlledClock in your development environment to manually test time-dependent features. Try doing that with a mock!

Type safety

Modern type checkers understand the Clock pattern perfectly. Monkey-patching often requires # type: ignore comments because you’re violating the type system.

Conclusion

The Clock pattern is well established and can be applied across all the programming languages I’ve used recently - I’m very interested to hear your thoughts about this pattern, please reach out on Slack or email to discuss further!