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 (inmock(),spy(), orinject()). 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 thanPackage->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 throughmock(), soScalar::Util::set_prototypeis not applied. Wrapping a prototyped function withspy()still emits aPrototype mismatchwarning. Usemock()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. Nesteddeep_mockblocks cause the inner exit to also tear down the outer mocks. Do not nestdeep_mockcalls. - 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()andasync_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::Privateis not activated). White-box tests int/unit.tcall private functions directly. IfSub::Privateenforcement 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.