NAME

Test::Mockingbird - Advanced mocking library for Perl with support for dependency injection, spies, call ordering, constructor interception, and async Future mocking

VERSION

Version 0.11

SYNOPSIS

use Test::Mockingbird;

# Mocking (shorthand form)
mock 'My::Module::method' => sub { 'mocked' };

# Mocking (longhand form)
mock('My::Module', 'method', sub { 'mocked' });

# Spying
my $spy = spy 'My::Module::method';
My::Module::method('arg1');
my @calls = $spy->();   # ( ['My::Module::method', 'arg1'], ... )

# Dependency injection
inject 'My::Module::Dependency' => $mock_object;

# Batch dependency injection
inject_all('My::Module', {
    DB     => $mock_db,
    Logger => $mock_logger,
});

# Constructor interception
intercept_new 'My::Service' => $stub_obj;
intercept_new 'My::Service' => sub { My::Double->new(@_[1..$#_]) };

# Unmock one layer
unmock 'My::Module::method';

# Restore everything
restore_all();

# Call ordering
spy 'A::fetch';
spy 'B::process';
A::fetch();
B::process();
assert_call_order('A::fetch', 'B::process');
clear_call_log();

DESCRIPTION

Test::Mockingbird provides mocking, spying, dependency injection, call-order verification, and constructor interception for Perl test suites.

DIAGNOSTICS

diagnose_mocks

Returns a structured hashref of all active mock layers.

diagnose_mocks_pretty

Returns a human-readable multi-line string of all active mock layers.

Diagnostic Metadata

Each installed layer records:

type          -- category (mock, spy, inject, mock_return, ...)
installed_at  -- file and line number of the outermost user call site

LIMITATIONS

->can() may return truthy after unmocking a never-existed method

Perl's typeglob (GV) system auto-vivifies a GV entry the first time \&{$full_method} is called internally (in mock(), spy(), or inject()). After unmocking, this GV entry remains in the stash with an "undefined sub" placeholder in the CODE slot. Package->can('method') tests the GV's existence in the stash, not whether the CODE slot is defined, so it may still return a truthy value.

To test whether a sub is callable, use defined(&Package::method) rather than Package->can('method'). defined(&...) correctly returns false for the placeholder stub. Calling the stub dies with "Undefined subroutine".

Deleting the GV from the stash (via delete $stash{method}) would make ->can() return false but would break subsequent mock/inject stacking: compiled direct calls (Package::method()) cache the GV at compile time, so a new GV installed after a delete is invisible to those compiled calls.

Prototype mismatch warning from spy()

spy() installs its wrapper directly without going through mock(), so Scalar::Util::set_prototype is not applied. Wrapping a prototyped function with spy() still emits a Prototype mismatch warning. Use mock() with a delegating wrapper if warning-free wrapping is required.

No nested deep_mock scopes

Test::Mockingbird::DeepMock calls restore_all() at scope exit, which removes every active mock. Nested deep_mock blocks cause the inner exit to also tear down the outer mocks. Do not nest deep_mock calls.

Thread safety

The internal state (%mocked, %mock_meta, @call_log) is per-process lexical state. Concurrent threads that install and restore mocks will race. Do not use this module in threaded test harnesses without external locking.

Spy return value is a flat list

spy() and async_spy() return a coderef that yields a flat list of call records. A future version may return an arrayref to reduce stack pressure; the API is not yet changed to avoid breaking callers.

Private-function encapsulation

Functions prefixed with _ are private by convention but are not enforced at runtime (Sub::Private is not activated). White-box tests in t/unit.t call private functions directly. If Sub::Private enforcement is added, a testing-interface export mechanism will be required.

METHODS

mock

Replace a method with a coderef.

mock('My::Module', 'method', sub { 'mocked' });
mock 'My::Module::method' => sub { 'mocked' };

Mocks stack in LIFO order. Each mock() call saves the current CODE slot (or the auto-vivified undef stub if the method does not exist) and installs the replacement. unmock() pops one layer; restore_all() drains all.

If the original carries a Perl prototype, the same prototype is stamped onto the replacement coderef before installation, suppressing Prototype mismatch warnings.

API SPECIFICATION

Input

target      -- Str, 'Pkg::method' or ('Pkg', 'method')
replacement -- CodeRef

Output

returns: undef

MESSAGES

"Package, method and replacement are required" -- target or coderef missing

unmock

Restore the previous implementation of a mocked method (one layer).

unmock('My::Module', 'method');
unmock 'My::Module::method';

If the method did not exist before it was mocked, the original undef-stub is restored so that calling the method dies with "Undefined subroutine". Note: ->can() may still return truthy; use defined(&...) to test whether a method is callable. See "LIMITATIONS".

API SPECIFICATION

Input

target -- Str, 'Pkg::method' or ('Pkg', 'method')

Output

returns: undef

MESSAGES

"Package and method are required for unmocking" -- target missing

mock_scoped

Create a scoped mock that restores automatically when the guard goes out of scope.

Single-method forms

my $g = mock_scoped 'My::Module::method' => sub { 'mocked' };
my $g = mock_scoped('My::Module', 'method', sub { ... });

Multi-method forms

my $g = mock_scoped('My::Module',
    fetch  => sub { 'mocked_fetch'  },
    save   => sub { 'mocked_save'   },
);

my $g = mock_scoped(
    'My::Module::fetch'  => sub { 'mocked_fetch'  },
    'Other::Module::save' => sub { 'mocked_save'  },
);

All mocked methods are restored when $g goes out of scope.

API SPECIFICATION

Input

args -- four recognised forms (see above)

Output

returns: Test::Mockingbird::Guard

MESSAGES

"mock_scoped: unrecognised argument form" -- none of the four forms matched
"mock_scoped: expected coderef for '$target'" -- non-CODE value provided

spy

Wrap a method so that every call is recorded. The original method is still called and its return value is passed back to the caller.

my $spy = spy 'My::Module::method';
My::Module::method('arg');
my @calls = $spy->();   # ( ['My::Module::method', 'arg'], ... )
restore_all();

Returns a coderef that, when invoked, returns the list of captured call records. Each record is an arrayref [ $full_method, @args ].

Limitation

spy() does not call mock() internally and therefore does not apply prototype preservation. Wrapping a prototyped function emits a Prototype mismatch warning.

API SPECIFICATION

Input

target -- Str, 'Pkg::method' or ('Pkg', 'method')

Output

returns: CodeRef   # yields list of call records on invocation

MESSAGES

"Package and method are required for spying" -- target missing or incomplete

inject

Inject a mock dependency into a package.

inject('My::Module', 'Dependency', $mock_object);
inject 'My::Module::Dependency' => $mock_object;

Injecting undef is valid; use argument count (not definedness of the third argument) to distinguish shorthand from longhand.

API SPECIFICATION

Input

package    -- Str
dependency -- Str
value      -- Any (including undef)

Output

returns: undef

MESSAGES

"Package and dependency are required for injection" -- missing name

inject_all

Inject multiple dependencies into a package in one call.

inject_all('My::Service', {
    DB     => $mock_db,
    Logger => $mock_logger,
});

An empty hashref is a no-op. Each pair is equivalent to a separate inject() call and participates in the same mock stack.

API SPECIFICATION

Input

package      -- Str
dependencies -- HashRef

Output

returns: undef

MESSAGES

"inject_all requires a package name"            -- undef or empty package
"inject_all requires a hashref of dependencies" -- second arg not a HashRef

intercept_new

Intercept the new constructor of a class.

intercept_new 'My::Service' => $stub_obj;
intercept_new 'My::Service' => sub { My::Double->new(@_[1..$#_]) };

When given a plain value (including undef), every call to My::Service->new returns that value. When given a coderef, every call invokes the coderef with the original arguments (including the class name as the first argument) and returns its result.

This is a thin wrapper around mock(); restore_all(), unmock(), and diagnose_mocks() all work identically.

API SPECIFICATION

Input

class   -- Str (non-empty)
factory -- Any; CodeRef invoked per call, or scalar returned verbatim

Output

returns: undef

MESSAGES

"intercept_new requires a class name"                    -- undef/empty class
"intercept_new requires a replacement object or coderef" -- factory missing

restore_all

Restore all mocked methods and injected dependencies.

restore_all();            # restore everything
restore_all 'My::Module'; # restore only My::Module's mocks

When called with a package name, only mocks whose fully-qualified names begin with that package are restored. The call-order log is pruned to remove entries for the restored package.

API SPECIFICATION

Input

package -- Str, optional

Output

returns: undef

restore

Restore all mock layers for a single method target.

restore 'My::Module::method';

If the method was never mocked this is a no-op.

API SPECIFICATION

Input

target -- Str

Output

returns: undef

MESSAGES

"restore requires a target" -- undef target

mock_return

Mock a method to always return a fixed value.

mock_return 'My::Module::method' => 42;

API SPECIFICATION

Input

target -- Str
value  -- Any

Output

returns: undef

MESSAGES

"mock_return requires a target and a value" -- target undefined

mock_exception

Mock a method to always throw an exception.

mock_exception 'My::Module::method' => 'something went wrong';

API SPECIFICATION

Input

target  -- Str
message -- Str

Output

returns: undef

MESSAGES

"mock_exception requires a target and an exception message" -- either missing

mock_sequence

Mock a method to return a sequence of values over successive calls. The last value repeats when the sequence is exhausted.

mock_sequence 'My::Module::method' => (1, 2, 3);

API SPECIFICATION

Input

target -- Str
values -- Array (one or more)

Output

returns: undef

MESSAGES

"mock_sequence requires a target and at least one value" -- empty value list

mock_once

Install a mock that fires exactly once. After the first call the previous implementation is automatically restored.

mock_once 'My::Module::method' => sub { 'temporary' };

API SPECIFICATION

Input

target -- Str
code   -- CodeRef

Output

returns: undef

MESSAGES

"mock_once requires a target and a coderef" -- missing or non-CODE factory

PSEUDOCODE

parse target → (package, method)
wrapper = sub {
    result = code(@_)
    unmock(package, method)   -- pop this very layer
    return result
}
install wrapper via mock() with TYPE='mock_once'

assert_call_order

Assert that the named methods were called in left-to-right order.

assert_call_order('A::fetch', 'B::process', 'C::save');

Produces one TAP ok/not-ok line and returns a boolean. Intervening calls to other methods are ignored.

API SPECIFICATION

Input

methods -- Array of Str (two or more fully-qualified names)

Output

returns: Bool

MESSAGES

"assert_call_order requires at least two method names" -- fewer than two given

clear_call_log

Clear the call-order log without restoring mocks or spies.

clear_call_log();

restore_all() also clears the log automatically.

API SPECIFICATION

Input

none

Output

returns: undef

diagnose_mocks

Return a structured hashref of all currently active mock layers.

my $diag = diagnose_mocks();
# $diag->{'My::Pkg::method'} = {
#   depth            => 1,
#   layers           => [ { type => 'mock_return', installed_at => '...' } ],
# }

API SPECIFICATION

Input

none

Output

returns: HashRef

diagnose_mocks_pretty

Return a human-readable multi-line string of all active mock layers.

API SPECIFICATION

Input

none

Output

returns: Str

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

FORMAL SPECIFICATION

mock

mock ≙
  ∀ target : Str; replacement : CodeRef •
    pre  target ≠ '' ∧ defined(replacement)
    post mocked'[target] = ⟨saved(target)⟩ ⌢ mocked[target]
         ∧ sym_table'[target].CODE = replacement
         ∧ prototype(replacement) = prototype(saved(target))

unmock

unmock ≙
  ∀ target : Str •
    let prev = head(mocked[target]) •
      post mocked'[target] = tail(mocked[target])
           ∧ sym_table'[target].CODE = prev
           ∧ mock_meta'[target] = tail(mock_meta[target])

mock_scoped

mock_scoped ≙
  install all mocks via mock()
  ∧ return Guard(full_methods)
  ∧ Guard.DESTROY ⇒ ∀ m ∈ full_methods • unmock(m)

spy

spy ≙
  ∀ target : Str •
    pre  defined(target)
    post sym_table'[target].CODE = wrapper(orig)
         ∧ wrapper: @args → (calls' = calls ⌢ ⟨[target, @args]⟩ ∧ orig(@args))

inject

inject ≙
  ∀ pkg : Str; dep : Str; val : Any •
    pre  pkg ≠ '' ∧ dep ≠ ''
    post sym_table'["${pkg}::${dep}"].CODE = sub { val }

inject_all

inject_all ≙
  ∀ pkg : Str; deps : HashRef •
    post ∀ (k,v) ∈ deps • inject(pkg, k, v)

intercept_new

intercept_new ≙
  ∀ class : Str; factory : Any •
    pre  class ≠ '' ∧ @args ≥ 2
    let  rep = (factory : CodeRef) ? factory : sub { factory } •
      post mock("${class}::new", rep)

restore_all

restore_all ≙
  global: mocked' = {} ∧ mock_meta' = {} ∧ call_log' = []
  scoped: ∀ target ∈ dom(mocked) • target =~ /^pkg::/ ⇒ unmock_all(target)
          ∧ call_log' = [ e ∈ call_log | e !~ /^pkg::/ ]

restore

restore ≙
  ∀ target : Str •
    pre  defined(target)
    post mocked[target] = []

mock_return

mock_return ≙
  ∀ target : Str; value : Any •
    post sym_table'[target].CODE = sub { value }

mock_exception

mock_exception ≙
  ∀ target : Str; msg : Str •
    post sym_table'[target].CODE = sub { croak msg }

mock_sequence

mock_sequence ≙
  ∀ target : Str; values : Seq(Any) •
    pre  |values| ≥ 1
    post let queue = values •
      sym_table'[target].CODE = sub { head(queue) if |queue|=1 else shift(queue) }

mock_once

mock_once ≙
  ∀ target : Str; code : CodeRef •
    post sym_table'[target] = sub {
      result = code(@args)
      unmock(target)
      return result
    }

assert_call_order

assert_call_order ≙
  ∀ expected : Seq(Str) •
    pre  |expected| ≥ 2
    post result = (∀ i • ∃ p_i : ℕ | p_0 < p_1 < … ∧ call_log[p_i] = expected[i])

clear_call_log

clear_call_log ≙ post call_log' = []

diagnose_mocks

diagnose_mocks ≙
  returns { target ↦ { depth, layers } | target ∈ dom(mocked) }

diagnose_mocks_pretty

diagnose_mocks_pretty ≙ stringify(diagnose_mocks())

LICENCE AND COPYRIGHT

Copyright 2025-2026 Nigel Horne.

Usage is subject to the GPL2 licence terms. If you use it, please let me know.