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_time

    Freezes time at a specific timestamp. All subsequent calls to now() return the frozen value until time is restored.

  • travel_to

    Moves the frozen clock to a new timestamp.

  • advance_time / rewind_time

    Moves the frozen clock forward or backward by a duration, expressed in seconds, minutes, hours, or days.

  • with_frozen_time

    Temporarily overrides time inside a code block, restoring the previous state afterward - even if the block dies.

  • restore_all

    Restores 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's time() to avoid global monkey-patching.

  • now() is safe to call inside nested with_frozen_time blocks.

  • When writing modules intended for testing, prefer calling now() over time() 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 $ACTIVE to 1)

  • Sets both $CURRENT_EPOCH and $BASE_EPOCH to the parsed timestamp

  • Causes 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_EPOCH to the parsed timestamp.

  • Leaves $ACTIVE set to 1 (time remains frozen).

  • Does not modify $BASE_EPOCH; only freeze_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
    days

    If 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 $ACTIVE set to 1 (time remains frozen).

Notes

  • advance_time() does not modify $BASE_EPOCH; only freeze_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
    days

    If 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 $ACTIVE set to 1 (time remains frozen).

Notes

  • rewind_time() does not modify $BASE_EPOCH; only freeze_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 $ACTIVE to 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.