NAME
Test::Mockingbird::TimeTravel - Deterministic, controllable time for Perl tests
VERSION
Version 0.07
SYNOPSIS
use Test::Mockingbird::TimeTravel qw(
now
freeze_time
travel_to
advance_time
rewind_time
restore_all
with_frozen_time
);
# Freeze time at a known point
freeze_time('2025-01-01T00:00:00Z');
is now(), 1735689600, 'time is frozen';
# Move the frozen clock forward
advance_time(2 => 'minutes');
is now(), 1735689720, 'time advanced deterministically';
# Temporarily override time inside a block
with_frozen_time '2025-01-02T12:00:00Z' => sub {
is now(), 1735819200, 'block sees overridden time';
};
# After the block, the previous frozen time is restored
is now(), 1735689720, 'outer time restored';
# Return to real system time
restore_all();
isnt now(), 1735689720, 'real time restored';
DESCRIPTION
Test::Mockingbird::TimeTravel provides a lightweight, deterministic time-control layer for Perl tests. It allows you to freeze time, move it forward or backward, jump to specific timestamps, and run code under a temporary time override - all without touching Perl's built-in time() or relying on global monkey-patching.
The module is designed for test suites that need:
predictable timestamps
repeatable behaviour across runs
clean separation between real time and simulated time
safe, nestable time overrides
Unlike traditional mocking of time(), TimeTravel does not replace Perl's core functions. Instead, it provides a dedicated now() function and a small set of declarative operations that manipulate an internal, frozen clock. This avoids global side effects and makes time behaviour explicit in your tests.
Core Concepts
now()Returns the current simulated time if time is frozen, or the real system time otherwise.
freeze_timeFreezes time at a specific timestamp. All subsequent calls to
now()return the frozen value until time is restored.travel_toMoves the frozen clock to a new timestamp.
advance_time/rewind_timeMoves the frozen clock forward or backward by a duration, expressed in seconds, minutes, hours, or days.
with_frozen_timeTemporarily overrides time inside a code block, restoring the previous state afterward - even if the block dies.
restore_allRestores real time and clears all frozen state.
TimeTravel is fully compatible with Test::Mockingbird::DeepMock, which can apply time-travel plans declaratively as part of a larger mocking scenario.
now
Return the current time according to the TimeTravel engine.
Purpose
now() provides a deterministic replacement for Perl's built-in time() when writing tests. If time is frozen (via freeze_time, travel_to, advance_time, or rewind_time), now() returns the simulated epoch value. If time is not frozen, it returns the real system time.
This allows test suites to avoid nondeterministic behaviour caused by wall-clock time, while still permitting explicit control over temporal flow.
Arguments
None. now() takes no parameters.
Returns
An integer epoch timestamp:
the simulated time if TimeTravel is active
the real system time (
CORE::time()) if TimeTravel is inactive
Side Effects
None. now() does not modify internal state; it only reads the current frozen or real time.
Notes
now()is intentionally separate from Perl'stime()to avoid global monkey-patching.now()is safe to call inside nestedwith_frozen_timeblocks.When writing modules intended for testing, prefer calling
now()overtime()so that behaviour can be controlled deterministically.
Example
use Test::Mockingbird::TimeTravel qw(now freeze_time restore_all);
freeze_time('2025-01-01T00:00:00Z');
my $t1 = now(); # deterministic epoch
advance_time(60);
my $t2 = now(); # exactly 60 seconds later
restore_all();
my $t3 = now(); # real system time again
API
Input (Params::Validate::Strict)
now()
Input schema:
{
params => [],
named => 0,
}
Output (Returns::Set)
returns: Int
Output schema:
{
returns => 'Int', # epoch seconds
}
freeze_time
Freeze the TimeTravel clock at a specific timestamp.
Purpose
freeze_time() activates the TimeTravel engine and sets the simulated clock to a deterministic epoch value. Once frozen, all calls to now() return the frozen time until it is changed by travel_to, advance_time, rewind_time, or restored via restore_all.
This is the primary entry point for deterministic time control in tests.
Arguments
freeze_time($timestamp)
Takes a single required argument:
$timestamp- a timestamp in any format supported by_parse_timestamp, including:YYYY-MM-DD YYYY-MM-DD HH:MM:SS YYYY-MM-DDTHH:MM:SSZ raw epoch seconds
Returns
An integer epoch value representing the frozen time.
Side Effects
Activates the TimeTravel engine (sets
$ACTIVEto 1)Sets both
$CURRENT_EPOCHand$BASE_EPOCHto the parsed timestampCauses all subsequent calls to
now()to return the frozen epoch
Notes
Calling
freeze_time()when time is already frozen simply overwrites the current frozen value.The frozen time persists until explicitly changed or restored.
Use
restore_all()to return to real system time.
Example
use Test::Mockingbird::TimeTravel qw(now freeze_time restore_all);
my $t = freeze_time('2025-01-01T00:00:00Z');
is $t, 1735689600, 'freeze_time returns epoch';
is now(), 1735689600, 'now() returns frozen time';
advance_time(120);
is now(), 1735689720, 'time advanced deterministically';
restore_all();
isnt now(), 1735689720, 'real time restored';
API
Input (Params::Validate::Strict)
freeze_time($timestamp)
Input schema:
{
params => [
{ type => 'Str | Int' }, # timestamp in any supported format
],
named => 0,
}
Output (Returns::Set)
returns: Int
Output schema:
{
returns => 'Int', # epoch seconds
}
travel_to
Move the frozen TimeTravel clock to a new timestamp.
Purpose
travel_to() updates the simulated time to a specific timestamp while keeping the TimeTravel engine active. It is used to jump directly to a new moment without unfreezing time or altering the fact that time is currently frozen.
This is useful for tests that need to simulate large jumps in time instantly, such as expiring sessions, rotating logs, or advancing scheduled events.
Arguments
travel_to($timestamp)
Takes a single required argument:
$timestamp- a timestamp in any format supported by_parse_timestamp, including:YYYY-MM-DD YYYY-MM-DD HH:MM:SS YYYY-MM-DDTHH:MM:SSZ raw epoch seconds
Returns
An integer epoch value representing the new simulated time.
Side Effects
Croaks if called while TimeTravel is inactive.
Updates
$CURRENT_EPOCHto the parsed timestamp.Leaves
$ACTIVEset to 1 (time remains frozen).Does not modify
$BASE_EPOCH; onlyfreeze_time()sets the base.
Notes
travel_to()cannot be used unless time has already been frozen.To temporarily override time inside a block, use
with_frozen_time()instead.travel_to()is deterministic and does not depend on real system time.
Example
use Test::Mockingbird::TimeTravel qw(
now freeze_time travel_to restore_all
);
freeze_time('2025-01-01T00:00:00Z');
is now(), 1735689600, 'initial freeze';
travel_to('2025-01-03T12:34:56Z');
is now(), 1735907696, 'jumped to new timestamp';
restore_all();
isnt now(), 1735907696, 'real time restored';
API
Input (Params::Validate::Strict)
travel_to($timestamp)
Input schema:
{
params => [
{ type => 'Str | Int' }, # timestamp in any supported format
],
named => 0,
}
Output (Returns::Set)
returns: Int
Output schema:
{
returns => 'Int', # epoch seconds
}
advance_time
Advance the frozen TimeTravel clock by a specified duration.
Purpose
advance_time() moves the simulated clock forward by a given amount of time. It is used to model the passage of time in a deterministic way while the TimeTravel engine is active. This is especially useful for testing expiry windows, retry backoff, cache TTLs, and any logic that depends on elapsed time.
Arguments
advance_time($amount, $unit)
Takes two arguments:
$amount- a positive or negative integer representing the magnitude of the time shift$unit- an optional unit string. Supported units:second seconds minute minutes hour hours day daysIf no unit is provided, the amount is interpreted as raw seconds.
Returns
An integer epoch value representing the new simulated time after the advance.
Side Effects
Croaks if called while TimeTravel is inactive.
Converts the amount and unit into seconds.
Adds the computed delta to
$CURRENT_EPOCH.Leaves
$ACTIVEset to 1 (time remains frozen).
Notes
advance_time()does not modify$BASE_EPOCH; onlyfreeze_time()sets the base.Negative amounts are allowed but uncommon; use
rewind_time()for clarity.The operation is deterministic and independent of real system time.
Example
use Test::Mockingbird::TimeTravel qw(
now freeze_time advance_time restore_all
);
freeze_time('2025-01-01T00:00:00Z');
is now(), 1735689600, 'initial freeze';
advance_time(30);
is now(), 1735689630, 'advanced 30 seconds';
advance_time(2 => 'minutes');
is now(), 1735689750, 'advanced 2 minutes';
restore_all();
isnt now(), 1735689750, 'real time restored';
API
Input (Params::Validate::Strict)
advance_time($amount, $unit)
Input schema:
{
params => [
{ type => 'Int' }, # amount
{ type => 'Str', optional => 1 }, # unit
],
named => 0,
}
Output (Returns::Set)
returns: Int
Output schema:
{
returns => 'Int', # epoch seconds
}
rewind_time
Rewind the frozen TimeTravel clock by a specified duration.
Purpose
rewind_time() moves the simulated clock backward by a given amount of time. It is the inverse of advance_time() and is used to test logic that depends on earlier timestamps, negative offsets, or rollback scenarios, all in a deterministic and controlled way.
Arguments
rewind_time($amount, $unit)
Takes two arguments:
$amount- a positive or negative integer representing the magnitude of the time shift$unit- an optional unit string. Supported units:second seconds minute minutes hour hours day daysIf no unit is provided, the amount is interpreted as raw seconds.
Returns
An integer epoch value representing the new simulated time after the rewind.
Side Effects
Croaks if called while TimeTravel is inactive.
Converts the amount and unit into seconds.
Subtracts the computed delta from
$CURRENT_EPOCH.Leaves
$ACTIVEset to 1 (time remains frozen).
Notes
rewind_time()does not modify$BASE_EPOCH; onlyfreeze_time()sets the base.Negative amounts are allowed but uncommon; use
advance_time()for clarity when moving forward.The operation is deterministic and independent of real system time.
Example
use Test::Mockingbird::TimeTravel qw(
now freeze_time rewind_time restore_all
);
freeze_time('2025-01-01T00:00:00Z');
is now(), 1735689600, 'initial freeze';
rewind_time(30);
is now(), 1735689570, 'rewound 30 seconds';
rewind_time(1 => 'hour');
is now(), 1735685970, 'rewound 1 hour';
restore_all();
isnt now(), 1735685970, 'real time restored';
API
Input (Params::Validate::Strict)
rewind_time($amount, $unit)
Input schema:
{
params => [
{ type => 'Int' }, # amount
{ type => 'Str', optional => 1 }, # unit
],
named => 0,
}
Output (Returns::Set)
returns: Int
Output schema:
{
returns => 'Int', # epoch seconds
}
restore_all
Restore real time and clear all TimeTravel state.
Purpose
restore_all() deactivates the TimeTravel engine and returns the system to normal, non-frozen time. After calling this function, now() once again returns the real system time from CORE::time().
This is the canonical way to end a time-travel scenario in tests.
Arguments
None. restore_all() takes no parameters.
Returns
Nothing. The function returns an undefined value.
Side Effects
Sets
$ACTIVEto 0, disabling frozen time.Clears
$CURRENT_EPOCH.Clears
$BASE_EPOCH.Causes all subsequent calls to
now()to return real system time.
Notes
restore_all()is idempotent; calling it multiple times is safe.It is automatically invoked by Test::Mockingbird::DeepMock when a time plan is used.
Use this function at the end of tests to ensure no frozen state leaks into later tests.
Example
use Test::Mockingbird::TimeTravel qw(
now freeze_time advance_time restore_all
);
freeze_time('2025-01-01T00:00:00Z');
advance_time(60);
is now(), 1735689660, 'time is frozen and advanced';
restore_all();
isnt now(), 1735689660, 'real time restored';
API
Input (Params::Validate::Strict)
restore_all()
Input schema:
{
params => [],
named => 0,
}
Output (Returns::Set)
returns: Undef
Output schema:
{
returns => 'Undef',
}
with_frozen_time
Temporarily override the TimeTravel clock inside a code block.
Purpose
with_frozen_time() runs a block of code under a temporary frozen timestamp, restoring the previous time state afterward. This allows tests to simulate nested or scoped time overrides without permanently altering the global TimeTravel state.
It is the safest way to test code that depends on time within a limited scope, especially when combined with freeze_time(), travel_to(), advance_time(), or rewind_time().
Arguments
with_frozen_time($timestamp, $code)
Takes two required arguments:
$timestamp- a timestamp in any format supported by_parse_timestamp, including:YYYY-MM-DD YYYY-MM-DD HH:MM:SS YYYY-MM-DDTHH:MM:SSZ raw epoch seconds$code- a coderef to execute while the override is active
Returns
Returns whatever the code block returns. In list context, returns a list. In scalar context, returns the block's scalar result. In void context, returns nothing.
Side Effects
Saves the current TimeTravel state (active flag, current epoch, base epoch).
Activates frozen time using the provided timestamp.
Executes the code block under the overridden time.
Restores the previous TimeTravel state after the block completes, even if the block throws an exception.
Rethrows any exception from inside the block.
Notes
with_frozen_time()is fully nestable; each invocation restores its own state.It does not require time to be frozen beforehand.
It is ideal for testing code that performs multiple time-based operations in different scopes.
Example
use Test::Mockingbird::TimeTravel qw(
now freeze_time with_frozen_time restore_all
);
freeze_time('2025-01-01T00:00:00Z');
my $outer = now(); # 1735689600
my $inner;
with_frozen_time '2025-01-02T00:00:00Z' => sub {
$inner = now(); # 1735776000
};
is $inner, 1735776000, 'inner block saw overridden time';
is now(), $outer, 'outer frozen time restored';
restore_all();
API
Input (Params::Validate::Strict)
with_frozen_time($timestamp, $code)
Input schema:
{
params => [
{ type => 'Str | Int' }, # timestamp
{ type => 'CodeRef' }, # block to execute
],
named => 0,
}
Output (Returns::Set)
returns: Any
Output schema:
{
returns => 'Any', # whatever the block returns
}
SUPPORT
This module is provided as-is without any warranty.
Please report any bugs or feature requests to bug-test-mockingbird at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-Mockingbird. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
You can find documentation for this module with the perldoc command.
perldoc Test::Mockingbird::TimeTravel
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
BUGS
SEE ALSO
REPOSITORY
https://github.com/nigelhorne/Test-Mockingbird
SUPPORT
This module is provided as-is without any warranty.
LICENCE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to licence terms.
The licence terms of this software are as follows:
Personal single user, single computer use: GPL2
All other users (including Commercial, Charity, Educational, Government) must apply in writing for a licence for use from Nigel Horne at the above e-mail.