NAME

Test::Mockingbird::DeepMock - Declarative, structured mocking and spying for Perl tests

VERSION

Version 0.07

SYNOPSIS

use Test::Mockingbird::DeepMock qw(deep_mock);

{
    package MyApp;
    sub greet  { "hello" }
    sub double { $_[1] * 2 }
}

deep_mock(
    {
        mocks => [
            {
                target => 'MyApp::greet',
                type   => 'mock',
                with   => sub { "mocked" },
            }, {
                target => 'MyApp::double',
                type   => 'spy',
                tag    => 'double_spy',
            },
        ], expectations => [
            {
                tag   => 'double_spy',
                calls => 2,
            },
        ],
    },
    sub {
        is MyApp::greet(), 'mocked', 'greet() was mocked';

        MyApp::double(2);
        MyApp::double(3);
    }
);

DESCRIPTION

Test::Mockingbird::DeepMock provides a declarative, data-driven way to describe mocking, spying, injection, and expectations in Perl tests.

Instead of scattering mock, spy, and restore_all calls throughout your test code, DeepMock lets you define a complete mocking plan in a single hashref, then executes your test code under that plan.

This produces tests that are:

  • easier to read

  • easier to maintain

  • easier to extend

  • easier to reason about

DeepMock is built on top of Test::Mockingbird, adding structure, expectations, and a clean DSL.

WHY DEEP MOCK?

Traditional mocking in Perl tends to be:

  • imperative

  • scattered across the test body

  • difficult to audit

  • easy to forget to restore

DeepMock solves these problems by letting you declare everything up front:

deep_mock(
    {
        mocks        => [...],
        expectations => [...],
    },
    sub { ... }
);

This gives you:

  • a single place to see all mocks and spies

  • automatic restore of all mocks

  • structured expectations

  • reusable patterns

  • a clean separation between setup and test logic

PLAN STRUCTURE

A DeepMock plan is a hashref with the following keys:

mocks

An arrayref of mock specifications. Each entry is a hashref:

{
    target => 'Package::method',   # required
    type   => 'mock' | 'spy' | 'inject',
    with   => sub { ... },         # for mock/inject
    tag    => 'identifier',        # for spies or scoped mocks
    scoped => 1,                   # optional
}

Types

mock

Replaces the target method with the provided coderef.

spy

Wraps the method and records all calls. Must have a tag.

inject

Injects a value or behavior into the target (delegates to Test::Mockingbird::inject).

expectations

An arrayref of expectation specifications. Each entry is a hashref:

{
    tag   => 'double_spy',   # required
    calls => 2,              # optional
    args_like => [           # optional
        [ qr/foo/, qr/bar/ ],
    ],
}

Expectation fields

tag

Identifies which spy this expectation applies to.

calls

Expected number of calls.

args_eq

Arrayref of arrayrefs. Each inner array lists exact argument values expected for a specific call. Values are compared with Test::More::is.

args_deeply

Arrayref of arrayrefs. Each inner array lists deep structures to compare against the arguments for a specific call. Uses Test::Deep::cmp_deeply.

args_like

Arrayref of arrayrefs of regexes. Each inner array describes expected arguments for a specific call.

never

Asserts that the spy was never called. Mutually exclusive with calls.

globals

Optional hashref controlling global behavior:

globals => {
    restore_on_scope_exit => 1,   # default
}

time

Optional hashref describing a time-travel plan to apply while the deep_mock block is running. This integrates with Test::Mockingbird::TimeTravel and allows declarative control of frozen time, time jumps, and temporal overrides.

If provided, the time plan is applied:

1. before any mocks are installed
2. before the test code block is executed
3. automatically restored after the block completes

A time plan may include any of the following keys:

time => {
    freeze  => '2025-01-01T00:00:00Z',
    travel  => '2025-01-02T12:00:00Z',
    advance => [ 2 => 'minutes' ],
    rewind  => [ 1 => 'hour'    ],
}

freeze

Freezes time at the given timestamp. Accepts any format supported by Test::Mockingbird::TimeTravel, including:

  • YYYY-MM-DD

  • YYYY-MM-DD HH:MM:SS

  • YYYY-MM-DDTHH:MM:SSZ

  • raw epoch seconds

travel

Moves the frozen clock to a new timestamp without unfreezing time.

advance

Advances the frozen clock by a duration. Must be an arrayref:

advance => [ $amount => $unit ]

Units may be seconds, minutes, hours, or days.

rewind

Rewinds the frozen clock by a duration. Same format as advance.

Example

deep_mock(
    {
        time => {
            freeze  => '2025-01-01T00:00:00Z',
            advance => [ 2 => 'minutes' ],
        },
        mocks => [
            {
                target => 'MyApp::stamp',
                type   => 'mock',
                with   => sub { now() },   # observes frozen time
            },
        ],
    },
    sub {
        is MyApp::stamp(),
           Test::Mockingbird::TimeTravel::_parse_datetime(
               '2025-01-01T00:02:00Z'
           ),
           'mock sees advanced frozen time';
    }
);

Restoration

All time-travel state is automatically restored after the deep_mock block completes, regardless of whether the block returns normally or dies. This mirrors the automatic restoration of mocks.

COOKBOOK

Mocking a method

mocks => [
    {
        target => 'MyApp::greet',
        type   => 'mock',
        with   => sub { "hi" },
    },
]

Spying on a method

mocks => [
    {
        target => 'MyApp::double',
        type   => 'spy',
        tag    => 'dbl',
    },
]

Injecting a dependency

mocks => [
    {
        target => 'MyApp::Config::get',
        type   => 'inject',
        with   => { debug => 1 },
    },
]

Expecting a call count

expectations => [
    {
        tag   => 'dbl',
        calls => 3,
    },
]

Expecting argument patterns

expectations => [
    {
        tag      => 'dbl',
        args_like => [
            [ qr/^\d+$/ ],     # first call
            [ qr/^\d+$/ ],     # second call
        ],
    },
]

Combining mocking with time travel

DeepMock can apply a time-travel plan (via Test::Mockingbird::TimeTravel) before installing mocks. This allows tests to observe deterministic timestamps inside mocked methods or spies.

{
    package MyApp;
    sub stamp { time }   # original behaviour (non-deterministic)
    sub logit { $_[1] }
}

deep_mock(
    {
        time => {
            freeze  => '2025-01-01T00:00:00Z',
            advance => [ 2 => 'minutes' ],
        },
        mocks => [
            {
                target => 'MyApp::stamp',
                type   => 'mock',
                with   => sub { now() },   # observe frozen time
            },
            {
                target => 'MyApp::logit',
                type   => 'spy',
                tag    => 'log_spy',
            },
        ],
        expectations => [
            {
                tag   => 'log_spy',
                calls => 1,
                args_like => [
                    [ qr/^event:/ ],
                ],
            },
        ],
    },
    sub {
        my $t = MyApp::stamp();     # returns frozen + advanced time
        MyApp::logit("event:$t");   # spy records call + args
    }
);

In this example:

  • Time is frozen at 2025-01-01T00:00:00Z and advanced by two minutes before any mocks are installed.

  • MyApp::stamp is mocked to return now(), giving a deterministic timestamp inside the test.

  • MyApp::logit is spied on, and its arguments are validated against regex patterns.

  • After the block completes, both the mocking layer and the time-travel layer are automatically restored.

Full example

deep_mock(
    {
        mocks => [
            { target => 'A::foo', type => 'mock', with => sub { 1 } },
            { target => 'A::bar', type => 'spy',  tag => 'bar' },
        ],
        expectations => [
            { tag => 'bar', calls => 2 },
        ],
    },
    sub {
        A::foo();
        A::bar(10);
        A::bar(20);
    }
);

TROUBLESHOOTING

"Not enough arguments for deep_mock"

You are using the BLOCK prototype form:

deep_mock {
    ...
}, sub { ... };

This only works if deep_mock has a (&$) prototype AND the first argument is a real block, not a hashref.

DeepMock uses ($$) to avoid Perl's block-vs-hashref ambiguity.

Use parentheses instead:

deep_mock(
    { ... },
    sub { ... }
);

"Type of arg 1 must be block or sub {}"

You are still using the BLOCK prototype form. Switch to parentheses.

"Use of uninitialized value in multiplication"

Your spied method is being called with no arguments during spy installation. Make your method robust:

sub double { ($_[1] // 0) * 2 }

My mocks aren't restored

Ensure you didn't disable automatic restore:

globals => { restore_on_scope_exit => 0 }

Nested deep_mock scopes are not supported

DeepMock installs mocks using Test::Mockingbird, which provides only global restore semantics via restore_all. Because Test::Mockingbird does not expose a per-method restore API, DeepMock cannot safely restore only the mocks installed in an inner scope.

As a result, nested calls like:

deep_mock { ... } sub {
    deep_mock { ... } sub {
        ...
    };
};

will cause the inner restore to remove the outer mocks as well.

DeepMock therefore does not support nested mocking scopes.

deep_mock

Run a block of code with a set of mocks and expectations applied.

Purpose

Provides a declarative wrapper around Test::Mockingbird that installs mocks, runs a code block, and then validates expectations such as call counts and argument patterns.

Arguments

  • $plan - HashRef

    A plan describing mocks and expectations. Keys:

    • mocks - ArrayRef of mock specifications

      Each specification includes:

      - target - "Package::method" - type - "mock" or "spy" - with - coderef for mock behavior (mock only) - tag - identifier for later expectations

    • expectations - ArrayRef of expectation specifications

      Each specification includes:

      - tag - spy tag to validate - calls - expected call count - args_like - regex argument matching - args_eq - exact argument matching - args_deeply - deep structural matching - never - assert spy was not called

  • $code - CodeRef

    The block to execute while mocks are active.

Returns

Nothing. Dies on expectation failure.

Side Effects

Temporarily installs mocks and spies into the target packages. All mocks are removed after the code block completes.

Notes

This routine does not support nested deep_mock scopes. All mocks are global until restored.

API

Input (Params::Validate::Strict)

{
    mocks        => ArrayRef,
    expectations => ArrayRef,
},
CodeRef

Output (Returns::Set)

returns: undef

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::DeepMock