FrozenClock (FrozenClock v0.1.0)

Copy Markdown View Source

A minimal, async-safe wrapper around DateTime.utc_now/0 that lets tests freeze and travel time in the calling process.

Replace direct calls to DateTime.utc_now/0 with FrozenClock.utc_now/0 in production code. In tests, freeze/0, freeze/1, travel/1, and travel/2 pin or shift the clock for the current process only.

Isolation

All state lives in the process dictionary under a single key, so freezing time in one process never affects another. This makes async: true tests safe out of the box.

Spawned processes see real time

Freezing affects only the calling process. Code that spawns processes (Task.async/1, sending to a GenServer, Phoenix channels, ...) will see the real system time in those children. If you need cross-process control, use klotho.

Summary

Functions

Freezes time for the calling process.

Returns true when the calling process has frozen time.

Sets the frozen time to target.

Shifts the frozen time by amount of unit.

Removes any freeze for the calling process, restoring real time.

Returns the current time.

Functions

freeze()

@spec freeze() :: :ok

Freezes time for the calling process.

With no argument, freezes at the current real time. With a DateTime, freezes at that instant.

freeze(at)

@spec freeze(DateTime.t()) :: :ok

frozen?()

@spec frozen?() :: boolean()

Returns true when the calling process has frozen time.

travel(target)

@spec travel(DateTime.t()) :: :ok

Sets the frozen time to target.

If the process is not already frozen, this freezes it at target.

travel(amount, unit)

@spec travel(integer(), :day | :hour | :minute | System.time_unit()) :: :ok

Shifts the frozen time by amount of unit.

Raises if the calling process has not frozen time first.

Examples

iex> FrozenClock.freeze(~U[2026-01-01 00:00:00Z])
iex> FrozenClock.travel(1, :hour)
iex> FrozenClock.utc_now()
~U[2026-01-01 01:00:00Z]

unfreeze()

@spec unfreeze() :: :ok

Removes any freeze for the calling process, restoring real time.

Safe to call when time is not frozen.

utc_now()

@spec utc_now() :: DateTime.t()

Returns the current time.

When the calling process has frozen time, returns the frozen value; otherwise delegates to DateTime.utc_now/0.

Examples

iex> FrozenClock.freeze(~U[2026-01-01 00:00:00Z])
iex> FrozenClock.utc_now()
~U[2026-01-01 00:00:00Z]