NAME

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

VERSION

Version 0.12

SYNOPSIS

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

deep_mock(
    {
        mocks => [
            { target => 'MyApp::greet', type => 'mock', with => sub { 'hi' } },
            { target => 'MyApp::double', type => 'spy', tag => 'double_spy' },
        ],
        expectations => [
            { tag => 'double_spy', calls => 2 },
        ],
    },
    sub {
        is MyApp::greet(), 'hi', 'greet 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.

LIMITATIONS

Nested deep_mock scopes are not supported

deep_mock calls restore_all() on exit, which removes every active mock. A nested deep_mock call will cause the inner exit to also tear down the outer mocks.

inject type in mocks list

The inject mock type delegates to Test::Mockingbird::inject(). Because inject() builds a sub that returns the with value verbatim, passing a coderef via with stores the coderef itself; it is not called on each access. To inject a factory, wrap it explicitly.

PLAN STRUCTURE

mocks

ArrayRef of mock specs. Each entry:

{
    target => 'Package::method',
    type   => 'mock' | 'spy' | 'inject',
    with   => sub { ... },
    tag    => 'identifier',
}

expectations

ArrayRef of expectation specs. Each entry:

{
    tag        => 'spy_tag',
    calls      => $n,
    args_like  => [ [qr/pat1/, qr/pat2/], ... ],
    args_eq    => [ ['exact', 'args'],    ... ],
    args_deeply => [ [$struct],           ... ],
    never      => 1,
    order      => [ 'A::m1', 'B::m2' ],
}

The order key is processed after all per-spy expectations and does not require a tag.

globals

Optional:

globals => { restore_on_scope_exit => 1 }

time

Optional time-travel plan applied before mocks are installed:

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

TROUBLESHOOTING

"Not enough arguments for deep_mock"

DeepMock uses ($$) prototype. Use deep_mock( {...}, sub { ... } ).

My mocks are not restored

Check globals = { restore_on_scope_exit => 0 }> has not been set.

METHODS

deep_mock

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

API SPECIFICATION

Input (Params::Validate::Strict schema)

$plan -- HashRef with keys: mocks, expectations, globals, time
$code -- CodeRef

Output (Returns::Set schema)

returns: whatever $code returns (context-sensitive)

MESSAGES

"deep_mock expects a HASHREF plan" -- first arg is not a hashref

FORMAL SPECIFICATION

deep_mock ≙
  ∀ plan : HashRef; code : CodeRef •
    pre  ref(plan) = 'HASH'
    post apply_time(plan.time)
         ∧ install_mocks(plan.mocks)
         ∧ result = code()
         ∧ check_expectations(plan.expectations)
         ∧ restore_all()

PSEUDOCODE

validate plan is HASH
apply time plan if present
install each mock/spy/inject from plan.mocks, building %handles
capture wantarray context
eval { run $code }
run expectations against %handles
restore all mocks (unless restore_on_scope_exit => 0)
restore time state
re-croak any exception from $code
return $code's result in correct context

SUPPORT

Please report bugs at https://github.com/nigelhorne/Test-Mockingbird/issues.

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

SEE ALSO

REPOSITORY

https://github.com/nigelhorne/Test-Mockingbird

LICENCE AND COPYRIGHT

Copyright 2026 Nigel Horne.

Usage is subject to GPL2 licence terms.

1 POD Error

The following errors were encountered while parsing the POD:

Around line 155:

Non-ASCII character seen before =encoding in '≙'. Assuming UTF-8