NAME

Test::Mockingbird::Async - Future-based async mocking for Test::Mockingbird

VERSION

Version 0.12

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 (Params::Validate::Strict schema)

- target: required, scalar string; shorthand 'Pkg::method' form - @values: zero or more values passed to Future-done>

Output (Returns::Set schema)

- return: undef

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 (Params::Validate::Strict schema)

- target: required, scalar string - $message: required, scalar string; the failure message - @details: optional; additional failure metadata passed to Future-fail>

Output (Returns::Set schema)

- return: undef

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 (Params::Validate::Strict schema)

- target: required, scalar string - @items: required; one or more plain values or Future objects

Output (Returns::Set schema)

- return: undef

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 (Params::Validate::Strict schema)

- target: required, scalar string - @values: zero or more values passed to Future-done>

Output (Returns::Set schema)

- return: undef

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 (Params::Validate::Strict schema)

- target: required; 'Pkg::method' shorthand or ('Pkg', 'method') longhand

Output (Returns::Set schema)

- return: coderef; when called, returns the list of call records

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.