TER1 (Statement): 31.08%
TER2 (Branch): 12.50%
TER3 (LCSAJ): -
Approximate LCSAJ segments: 17
โ Covered โ this LCSAJ path was executed during testing.
โ Not covered โ this LCSAJ path was never executed. These are the paths to focus on.
Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:
start โ end โ jump
Uncovered paths show [NOT COVERED] in the tooltip.
1: package Test::Mockingbird::Async; 2: 3: use strict; 4: use warnings; 5: 6: use Carp qw(croak); 7: use Exporter 'import'; 8: use Test::Mockingbird (); 9: 10: our @EXPORT_OK = qw( 11: mock_future_return 12: mock_future_fail 13: mock_future_sequence 14: mock_future_once 15: async_spy 16: ); 17: 18: =head1 NAME 19: 20: Test::Mockingbird::Async - Future-based async mocking for Test::Mockingbird 21: 22: =head1 VERSION 23: 24: Version 0.13 25: 26: =cut 27: 28: our $VERSION = '0.13'; 29: 30: =head1 SYNOPSIS 31: 32: use Test::Mockingbird; 33: use Test::Mockingbird::Async qw( 34: mock_future_return 35: mock_future_fail 36: mock_future_sequence 37: mock_future_once 38: async_spy 39: ); 40: 41: # Mock a method to return a pre-resolved Future 42: mock_future_return 'My::DB::fetch' => { id => 1 }; 43: my $result = My::DB::fetch()->get; # { id => 1 } 44: 45: # Mock a method to return a pre-failed Future 46: mock_future_fail 'My::DB::fetch' => 'not found'; 47: my ($msg) = My::DB::fetch()->failure; # 'not found' 48: 49: # Return different values over successive calls 50: mock_future_sequence 'My::DB::fetch' => (10, 20, 30); 51: My::DB::fetch()->get; # 10 52: My::DB::fetch()->get; # 20 53: My::DB::fetch()->get; # 30 (repeated from here) 54: 55: # Return a Future exactly once, then restore the previous implementation 56: mock_future_once 'My::DB::ping' => 'ok'; 57: My::DB::ping()->get; # 'ok', then original restored 58: 59: # Spy on a method that returns a Future 60: my $spy = async_spy 'My::DB::fetch'; 61: My::DB::fetch('key'); 62: my @calls = $spy->(); 63: my $call = $calls[0]; 64: # $call->{args} is [ 'My::DB::fetch', 'key' ] 65: # $call->{future} is the Future returned by the original 66: 67: restore_all(); 68: 69: =head1 DESCRIPTION 70: 71: C<Test::Mockingbird::Async> extends L<Test::Mockingbird> with helpers for 72: testing code that uses L<Future>-based asynchronous APIs. 73: 74: All mocks and spies installed by this module use the same underlying stack 75: as the core engine. C<restore_all()>, C<unmock()>, and C<diagnose_mocks()> 76: work identically to their core counterparts. Every installed layer is 77: recorded in C<diagnose_mocks()> with a type of C<mock_future_return>, 78: C<mock_future_fail>, C<mock_future_sequence>, C<mock_future_once>, or 79: C<async_spy>. 80: 81: C<async_spy> also writes to the call-order log, so 82: C<assert_call_order()> works across a mix of plain spies and async spies. 83: 84: =head2 Dependency 85: 86: This module requires the L<Future> distribution (available from CPAN). 87: It is not a mandatory dependency of L<Test::Mockingbird> itself; the 88: module loads C<Future> on first use and croaks with a helpful message if 89: it is not installed. 90: 91: =head1 METHODS 92: 93: =head2 mock_future_return 94: 95: Mock a method so that it always returns a pre-resolved C<Future>. 96: 97: mock_future_return 'My::DB::fetch' => $value; 98: mock_future_return 'My::DB::fetch' => ($val1, $val2); # multi-value 99: 100: The method is replaced with a stub that returns C<Future->done(@values)>. 101: Restore with C<restore_all()> or C<unmock()>. 102: 103: =head3 API specification 104: 105: =head4 Input (Params::Validate::Strict schema) 106: 107: - C<target>: required, scalar string; shorthand C<'Pkg::method'> form 108: - C<@values>: zero or more values passed to C<Future->done> 109: 110: =head4 Output (Returns::Set schema) 111: 112: - C<return>: undef 113: 114: =cut 115: 116: sub mock_future_return { 117: my ($target, @values) = @_; 118: 119: croak 'mock_future_return requires a target' unless defined $target; 120: 121: _require_future(); 122: 123: local $Test::Mockingbird::TYPE = 'mock_future_return'; 124: Test::Mockingbird::mock($target, sub { Future->done(@values) }); 125: 126: return; 127: } 128: 129: =head2 mock_future_fail 130: 131: Mock a method so that it always returns a pre-failed C<Future>. 132: 133: mock_future_fail 'My::DB::fetch' => 'not found'; 134: mock_future_fail 'My::DB::fetch' => ('db error', 'db', { code => 500 }); 135: 136: The method is replaced with a stub that returns 137: C<Future->fail($message, @details)>. The caller receives a rejected Future; 138: no exception is thrown at the call site. 139: 140: =head3 API specification 141: 142: =head4 Input (Params::Validate::Strict schema) 143: 144: - C<target>: required, scalar string 145: - C<$message>: required, scalar string; the failure message 146: - C<@details>: optional; additional failure metadata passed to C<Future->fail> 147: 148: =head4 Output (Returns::Set schema) 149: 150: - C<return>: undef 151: 152: =cut 153: 154: sub mock_future_fail { 155: my ($target, $message, @details) = @_; 156: 157: croak 'mock_future_fail requires a target and a failure message' 158: unless defined $target && defined $message; 159: 160: _require_future(); 161: 162: local $Test::Mockingbird::TYPE = 'mock_future_fail'; 163: Test::Mockingbird::mock($target, sub { Future->fail($message, @details) }); 164: 165: return; 166: } 167: 168: =head2 mock_future_sequence 169: 170: Mock a method so that it returns a sequence of C<Future> values over 171: successive calls. When the sequence is exhausted, the last item is repeated. 172: 173: mock_future_sequence 'My::DB::fetch' => (10, 20, 30); 174: My::DB::fetch()->get; # 10 175: My::DB::fetch()->get; # 20 176: My::DB::fetch()->get; # 30 (repeats) 177: 178: Each item in the sequence may be either a plain value (wrapped automatically 179: in C<Future->done>) or a pre-built C<Future> object (passed through as-is, 180: allowing a mix of resolved and failed Futures in the sequence): 181: 182: mock_future_sequence 'My::DB::fetch' => 183: 42, 184: Future->fail('oops'); 185: 186: =head3 API specification 187: 188: =head4 Input (Params::Validate::Strict schema) 189: 190: - C<target>: required, scalar string 191: - C<@items>: required; one or more plain values or C<Future> objects 192: 193: =head4 Output (Returns::Set schema) 194: 195: - C<return>: undef 196: 197: =cut 198: 199: sub mock_future_sequence { 200: my ($target, @items) = @_; 201: 202: croak 'mock_future_sequence requires a target and at least one item' 203: unless defined $target && @items; 204: 205: _require_future(); 206: 207: my @queue = @items; 208: 209: local $Test::Mockingbird::TYPE = 'mock_future_sequence'; 210: Test::Mockingbird::mock($target, sub { 211: my $item = @queue == 1 ? $queue[0] : shift @queue;212: # Pass pre-built Futures through unchanged; wrap plain values 213: return (ref $item && $item->isa('Future')) ? $item : Future->done($item);Mutants (Total: 1, Killed: 0, Survived: 1)
- NUM_BOUNDARY_211_21_!=: Numeric boundary flip == to !=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );214: }); 215: 216: return; 217: } 218: 219: =head2 mock_future_once 220: 221: Install a mock that returns a pre-resolved C<Future> exactly once. After the 222: first call the previous implementation is automatically restored. 223: 224: mock_future_return 'My::DB::ping' => 'baseline'; 225: mock_future_once 'My::DB::ping' => 'temporary'; 226: 227: My::DB::ping()->get; # 'temporary' 228: My::DB::ping()->get; # 'baseline' (previous mock restored) 229: 230: This is useful for simulating transient failures, one-time responses, or 231: state transitions in async code. 232: 233: =head3 API specification 234: 235: =head4 Input (Params::Validate::Strict schema) 236: 237: - C<target>: required, scalar string 238: - C<@values>: zero or more values passed to C<Future->done> 239: 240: =head4 Output (Returns::Set schema) 241: 242: - C<return>: undef 243: 244: =cut 245: 246: sub mock_future_once { 247: my ($target, @values) = @_; 248: 249: croak 'mock_future_once requires a target' unless defined $target; 250: 251: _require_future(); 252: 253: my ($package, $method) = Test::Mockingbird::_parse_target($target); 254: 255: my $wrapper = sub { 256: my $future = Future->done(@values); 257: Test::Mockingbird::unmock($package, $method); 258: return $future;Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_213_3: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_213_3: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );259: }; 260: 261: local $Test::Mockingbird::TYPE = 'mock_future_once'; 262: Test::Mockingbird::mock($target, $wrapper); 263: 264: return; 265: } 266: 267: =head2 async_spy 268: 269: Wrap a method so that every call is recorded along with the C<Future> it 270: returned. The original method is still called and its return value is 271: passed back to the caller unchanged. 272: 273: my $spy = async_spy 'My::DB::fetch'; 274: My::DB->fetch(id => 1); 275: 276: my @calls = $spy->(); 277: 278: my $call = $calls[0]; 279: # $call->{args} is [ 'My::DB::fetch', $invocant, id => 1 ] 280: # $call->{future} is whatever Future the original returned 281: 282: my $result = $call->{future}->get; # inspect resolved value 283: 284: Unlike the plain C<spy()> coderef (which returns arrayrefs), C<async_spy> 285: returns hashrefs so the C<future> field is unambiguous. 286: 287: C<async_spy> writes to the call-order log, so C<assert_call_order()> works 288: across a mix of plain and async spies. 289: 290: =head3 Limitations 291: 292: C<async_spy> assumes the spied method returns a C<Future>. If the method 293: returns a plain value the C<future> field in each call record will hold that 294: value rather than a C<Future>, and calling C<< ->get >> on it will fail. 295: 296: =head3 API specification 297: 298: =head4 Input (Params::Validate::Strict schema) 299: 300: - C<target>: required; C<'Pkg::method'> shorthand or C<('Pkg', 'method')> longhand 301: 302: =head4 Output (Returns::Set schema) 303: 304: - C<return>: coderef; when called, returns the list of call records 305: 306: =cut 307: 308: sub async_spy { 309: _require_future(); 310: 311: my ($package, $method) = Test::Mockingbird::_parse_target(@_); 312: 313: croak 'Package and method are required for async_spy' 314: unless $package && $method; 315: 316: my $full_method = "${package}::${method}"; 317: 318: # Capture current implementation so the wrapper can delegate to it. 319: # mock() will also capture it independently for stack bookkeeping; 320: # both captures see the same coderef at this point. 321: my $orig; 322: { 323: ## no critic (ProhibitNoStrict) 324: no strict 'refs'; 325: $orig = \&{$full_method}; 326: } 327: 328: my @calls; 329: 330: my $wrapper = sub { 331: my @call_args = @_; 332: my $future = $orig->(@call_args); 333: push @calls, { args => [$full_method, @call_args], future => $future }; 334: Test::Mockingbird::_record_call($full_method); 335: return $future;Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_258_3: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_258_3: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );336: }; 337: 338: local $Test::Mockingbird::TYPE = 'async_spy'; 339: Test::Mockingbird::mock($full_method, $wrapper); 340: 341: return sub { @calls };Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_335_3: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_335_3: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );342: } 343: 344: # ---------------------------------------------------------------------- 345: # Private helpers 346: # ---------------------------------------------------------------------- 347: 348: sub _require_future { 349: eval { require Future; 1 } 350: or croak "Test::Mockingbird::Async requires the Future module.\n" 351: . "Install it with: cpanm Future\n"; 352: } 353: 354: =head1 SUPPORT 355: 356: This module is provided as-is without any warranty. 357: 358: Please report bugs at L<https://github.com/nigelhorne/Test-Mockingbird/issues>. 359: 360: =head1 AUTHOR 361: 362: Nigel Horne, C<< <njh at nigelhorne.com> >> 363: 364: =head1 SEE ALSO 365: 366: =over 4 367: 368: =item * L<Test::Mockingbird> 369: 370: =item * L<Test::Mockingbird::DeepMock> 371: 372: =item * L<Future> 373: 374: =back 375: 376: =head1 LICENCE AND COPYRIGHT 377: 378: Copyright 2026 Nigel Horne. 379: 380: Usage is subject to the GPL2 licence terms. 381: If you use it, 382: please let me know. 383: 384: =cut 385: 386: 1;Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_341_2: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_341_2: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );