NAME
Test::Mockingbird::Async - Future-based async mocking for Test::Mockingbird
VERSION
Version 0.14
SYNOPSIS
use Test::Mockingbird;
use Test::Mockingbird::Async qw(
mock_future_return
mock_future_fail
mock_future_sequence
mock_future_once
async_spy
);
# Mock a method to return a pre-resolved Future
mock_future_return 'My::DB::fetch' => { id => 1 };
my $result = My::DB::fetch()->get; # { id => 1 }
# Mock a method to return a pre-failed Future
mock_future_fail 'My::DB::fetch' => 'not found';
my ($msg) = My::DB::fetch()->failure; # 'not found'
# Return different values over successive calls
mock_future_sequence 'My::DB::fetch' => (10, 20, 30);
My::DB::fetch()->get; # 10
My::DB::fetch()->get; # 20
My::DB::fetch()->get; # 30 (repeated from here)
# Return a Future exactly once, then restore the previous implementation
mock_future_once 'My::DB::ping' => 'ok';
My::DB::ping()->get; # 'ok', then original restored
# Spy on a method that returns a Future
my $spy = async_spy 'My::DB::fetch';
My::DB::fetch('key');
my @calls = $spy->();
my $call = $calls[0];
# $call->{args} is [ 'My::DB::fetch', 'key' ]
# $call->{future} is the Future returned by the original
restore_all();
DESCRIPTION
Test::Mockingbird::Async extends Test::Mockingbird with helpers for testing code that uses Future-based asynchronous APIs.
All mocks and spies installed by this module use the same underlying stack as the core engine. restore_all(), unmock(), and diagnose_mocks() work identically to their core counterparts. Every installed layer is recorded in diagnose_mocks() with a type of mock_future_return, mock_future_fail, mock_future_sequence, mock_future_once, or async_spy.
async_spy also writes to the call-order log, so assert_call_order() works across a mix of plain spies and async spies.
Dependency
This module requires the Future distribution (available from CPAN). It is not a mandatory dependency of Test::Mockingbird itself; the module loads Future on first use and croaks with a helpful message if it is not installed.
METHODS
mock_future_return
Mock a method so that it always returns a pre-resolved Future.
mock_future_return 'My::DB::fetch' => $value;
mock_future_return 'My::DB::fetch' => ($val1, $val2); # multi-value
The method is replaced with a stub that returns Future-done(@values)>. Restore with restore_all() or unmock().
API SPECIFICATION
Input
target -- Str, 'Pkg::method'
values -- List[Any], zero or more; passed to Future->done
Output
returns: undef
MESSAGES
"mock_future_return requires a target" -- target undef
"Test::Mockingbird::Async requires the Future module." -- Future missing
mock_future_fail
Mock a method so that it always returns a pre-failed Future.
mock_future_fail 'My::DB::fetch' => 'not found';
mock_future_fail 'My::DB::fetch' => ('db error', 'db', { code => 500 });
The method is replaced with a stub that returns Future-fail($message, @details)>. The caller receives a rejected Future; no exception is thrown at the call site.
API SPECIFICATION
Input
target -- Str, 'Pkg::method'
message -- Str, the failure message
details -- List[Any], optional; further arguments to Future->fail
Output
returns: undef
MESSAGES
"mock_future_fail requires a target and a failure message" -- either undef
"Test::Mockingbird::Async requires the Future module." -- Future missing
mock_future_sequence
Mock a method so that it returns a sequence of Future values over successive calls. When the sequence is exhausted, the last item is repeated.
mock_future_sequence 'My::DB::fetch' => (10, 20, 30);
My::DB::fetch()->get; # 10
My::DB::fetch()->get; # 20
My::DB::fetch()->get; # 30 (repeats)
Each item in the sequence may be either a plain value (wrapped automatically in Future-done>) or a pre-built Future object (passed through as-is, allowing a mix of resolved and failed Futures in the sequence):
mock_future_sequence 'My::DB::fetch' =>
42,
Future->fail('oops');
API SPECIFICATION
Input
target -- Str, 'Pkg::method'
items -- List[Any|Future], one or more; plain values are wrapped in
Future->done, Future objects are returned as they are
Output
returns: undef
MESSAGES
"mock_future_sequence requires a target and at least one item" -- target
undef or no items
"Test::Mockingbird::Async requires the Future module." -- Future missing
mock_future_once
Install a mock that returns a pre-resolved Future exactly once. After the first call the previous implementation is automatically restored.
mock_future_return 'My::DB::ping' => 'baseline';
mock_future_once 'My::DB::ping' => 'temporary';
My::DB::ping()->get; # 'temporary'
My::DB::ping()->get; # 'baseline' (previous mock restored)
This is useful for simulating transient failures, one-time responses, or state transitions in async code.
API SPECIFICATION
Input
target -- Str, 'Pkg::method'
values -- List[Any], zero or more; passed to Future->done
Output
returns: undef
MESSAGES
"mock_future_once requires a target" -- target undef
"Test::Mockingbird::Async requires the Future module." -- Future missing
async_spy
Wrap a method so that every call is recorded along with the Future it returned. The original method is still called and its return value is passed back to the caller unchanged.
my $spy = async_spy 'My::DB::fetch';
My::DB->fetch(id => 1);
my @calls = $spy->();
my $call = $calls[0];
# $call->{args} is [ 'My::DB::fetch', $invocant, id => 1 ]
# $call->{future} is whatever Future the original returned
my $result = $call->{future}->get; # inspect resolved value
Unlike the plain spy() coderef (which returns arrayrefs), async_spy returns hashrefs so the future field is unambiguous.
async_spy writes to the call-order log, so assert_call_order() works across a mix of plain and async spies.
Limitations
async_spy assumes the spied method returns a Future. If the method returns a plain value the future field in each call record will hold that value rather than a Future, and calling ->get on it will fail.
API SPECIFICATION
Input
target -- Str, 'Pkg::method' or ('Pkg', 'method')
Output
returns: CodeRef; yields the list of call records, each
{ args => [ 'Pkg::method', @args ], future => $returned }
MESSAGES
"Package and method are required for async_spy" -- target missing or incomplete
"Test::Mockingbird::Async requires the Future module." -- Future missing
FORMAL SPECIFICATION
Each function requires Future (_require_future) and installs one layer through Test::Mockingbird::mock() with type set to its own name; see "FORMAL SPECIFICATION" in Test::Mockingbird for mock, unmock and resolve.
mock_future_return
mock_future_return ≙
∀ target : Str; values : Seq(Any) •
pre defined(target)
post mock(target, sub { Future->done(values) })
mock_future_fail
mock_future_fail ≙
∀ target : Str; msg : Str; details : Seq(Any) •
pre defined(target) ∧ defined(msg)
post mock(target, sub { Future->fail(msg, details) })
mock_future_sequence
mock_future_sequence ≙
∀ target : Str; items : Seq(Any) •
pre defined(target) ∧ |items| ≥ 1
post let queue = items •
mock(target, sub {
let item = (|queue| = 1) ? head(queue) : shift(queue) •
item isa Future ? item : Future->done(item)
})
mock_future_once
mock_future_once ≙
∀ target : Str; values : Seq(Any) •
pre defined(target)
post mock(target, sub {
let f = Future->done(values) • unmock(target); f
})
async_spy
async_spy ≙
∀ pkg, meth : Str •
pre name(pkg) ∧ name(meth)
let t = pkg::meth; orig = resolve(pkg, meth) •
post mock(t, wrapper)
∧ wrapper(@args) ≙ let f = orig(@args) •
calls' = calls ⌢ ⟨{ args => [t, @args], future => f }⟩
∧ call_log' = call_log ⌢ ⟨t⟩; f
∧ returns sub { calls }
SUPPORT
This module is provided as-is without any warranty.
Please report bugs at https://github.com/nigelhorne/Test-Mockingbird/issues.
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
SEE ALSO
LICENCE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.