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.