Revision history for Test::Mockingbird - Advanced mocking library for Perl with support for dependency injection and spies

0.11	Wed Jul  8 12:43:17 EDT 2026

  [Bug fixes]

  - Fixed restore_all($pkg) scoped form: after draining the mock stack for a
    package's methods, the corresponding keys were not removed from %mocked
    and %mock_meta.  This caused diagnose_mocks() to continue reporting those
    methods as active even after restore_all('Pkg') had fully restored the
    original implementations.  Fixed by adding delete $mocked{$key} and
    delete $mock_meta{$key} inside the per-method loop, matching the behaviour
    of the global restore_all() form.  Discovered by a new integration test
    in t/integration.t.

  - Fixed "Prototype mismatch" warnings emitted by mock() when replacing a
    function that carries a Perl prototype (such as the () no-args prototype).
    The existing set_prototype() call correctly equates prototypes before the
    glob assignment, but on some Perl builds the GV-level prototype check fires
    before the CV slot update is fully visible, causing the warning to leak
    through.  Fixed by adding 'prototype' to the no warnings pragma in the
    glob-assignment block -- tightly scoped to that single statement so no
    legitimate warnings from surrounding code are affected.
    Reported by: https://www.cpantesters.org/cpan/report/42ed27a2-6477-11f1-a3bc-a055f9c4ba34

  [New features]

  - New module Test::Mockingbird::Async providing Future-based async mocking:

      mock_future_return($target, @values)
          Replaces the target with a stub returning Future->done(@values).

      mock_future_fail($target, $message, @details)
          Replaces the target with a stub returning Future->fail($message, @details).
          No exception is thrown at the call site; the caller receives a
          pre-failed Future.

      mock_future_sequence($target, @items)
          Returns each item in sequence over successive calls; last item
          repeats when exhausted.  Each item is either a plain value
          (wrapped in Future->done) or a pre-built Future (passed through).

      mock_future_once($target, @values)
          Returns Future->done(@values) on the first call, then automatically
          restores the previous implementation.

      async_spy($target)
          Wraps the target, records each call as a hashref
          { args => [$method, @args], future => $returned_future }, and
          returns a coderef that retrieves the call list.  Writes to the
          call-order log so assert_call_order() works across plain and
          async spies.

    All functions use the core mock stack; restore_all(), unmock(), and
    diagnose_mocks() work identically.  Future is an optional runtime
    dependency; the module croaks with a helpful message if it is absent.

  - intercept_new($class, $obj_or_coderef) intercepts the C<new> constructor
    of any class so that each call returns a controlled value instead of a
    real instance.  Two forms are supported:

      intercept_new 'My::Service' => $stub_obj;
          Every call to My::Service->new returns $stub_obj unchanged.
          Plain scalars, undef, and pre-built objects are all valid.

      intercept_new 'My::Service' => sub { ... };
          Every call invokes the coderef with the original arguments
          (including the class name as the first argument) and returns
          whatever the coderef returns.

    intercept_new is a thin wrapper around mock(), so the usual mock stack
    semantics apply: layers can be stacked and peeled with unmock(), the
    full stack is cleared by restore_all(), and diagnose_mocks() records
    each layer with type 'intercept_new'.  Works for classes with no own
    new() (inherited constructors are intercepted in the same way).

  - inject_all($package, \%deps) injects multiple mock dependencies into a
    package in a single call.  Each key in the hashref is treated as a
    dependency name; the corresponding value is passed to inject() as the
    mock object.  All injected dependencies are tracked by the usual mock
    stack and are restored by restore_all() or individual unmock() calls.
    An empty hashref is a no-op.  Exported by default alongside inject().

  - Call ordering: assert_call_order(@methods) verifies that spied methods
    were invoked in the declared left-to-right sequence.  Intervening calls
    to other methods are ignored; only the relative order of the named
    methods is checked.  Returns a boolean and emits a single TAP ok/not-ok.

  - clear_call_log() resets the call-order log without tearing down spies
    or mocks.  restore_all() continues to clear the log automatically.

  - Test::Mockingbird::DeepMock now accepts an C<order> key in the
    expectations array.  It takes an arrayref of fully-qualified method
    names and delegates to assert_call_order() after all per-spy
    expectations have been checked.  The key does not require a spy tag.

  [Bug fixes and refactoring]

  - Fixed unmock() meta-pop bug: previously deleted the entire %mock_meta key
    on every unmock call (wiping metadata for all lower layers); now correctly
    pops only the top-most meta entry to mirror the mock stack.

  - Fixed ghost-method bug in mock() and spy(): previously captured \&{method}
    without no-strict-refs, which under some conditions left stale CODE slots
    in the GV after restore.  Now always captures via no strict 'refs' so the
    same GV object is reused across mock/restore cycles.  Documented in
    LIMITATIONS that ->can() may return truthy for auto-vivified GVs; use
    defined(&Pkg::method) instead.

  - Fixed inject() to respect the $Test::Mockingbird::TYPE package variable
    so that callers can override the diagnostic layer type (matching the
    pattern used by mock_return, mock_exception, and Async functions).

  - Fixed restore_all($pkg) to prune @call_log entries for the specified
    package (previously cleared only %mocked and %mock_meta).

  - Added _caller_info() private helper that climbs the call stack past any
    Test::Mockingbird frame to record the user's actual call site in
    installed_at diagnostics (previously pointed to sugar-function internals).

  - Deduplicated DeepMock::_normalize_target(): now delegates to the
    authoritative Test::Mockingbird::_parse_target() to ensure consistent
    target parsing across the library.  The function name is kept as a thin
    wrapper for backwards-compatibility with white-box tests.

  - Fixed TimeTravel: die $err in with_frozen_time() changed to croak $err
    for consistency with the rest of the library.

  - VERSION POD in all four modules corrected from '0.10' to '0.11'.

  - Removed duplicate =head1 SUPPORT section from core module.

  - Removed spurious double '1;' at end of DeepMock.pm.

  [New files]

  - t/locales.t: POSIX locale tests verifying the module loads and croak
    error messages are locale-independent pure-Perl strings under
    en_US.UTF-8, de_DE.UTF-8, and ja_JP.UTF-8 LC_ALL settings.

  -  Bump minimum version: Fixes https://github.com/nigelhorne/Test-Mockingbird/issues/6

  [Tests]

  - Added t/intercept_new.t (14 subtests) covering: plain object, plain
    scalar, and undef return values; coderef factory with arg forwarding;
    factory returning a different class; restore_all and unmock restores;
    LIFO stacking of two interceptors; diagnose_mocks type; inherited
    constructor interception; combination with spy; and all error cases
    (undef class, empty class, missing factory argument).

  - Added t/async.t (24 subtests) covering all five Async exports: scalar,
    list, and undef return values; failure with and without category/detail;
    sequence ordering and last-item repeat; pre-built Future pass-through;
    mixed plain/Future sequences; once-fires-once; async_spy arg capture,
    Future capture, longhand target, diagnose_mocks type, and
    assert_call_order integration; and all error cases.

  - Added t/inject_all.t covering: basic multi-dependency injection, empty
    hashref no-op, restore_all cleanup, individual unmock of one injected
    dependency, error on missing package, error on non-hashref second arg,
    and diagnose_mocks recording all injected layers.

  - Added t/call_order.t covering: correct-order pass, wrong-order fail
    (return value verified via local $TODO), intervening-call tolerance,
    clear_call_log() mid-test reset, and the DeepMock order expectation.

  - Added call-ordering subtests to t/unit.t (_run_expectations order key)
    and t/integration.t (end-to-end via deep_mock).

0.10	Fri May  8 15:11:42 EDT 2026

  [Bug fixes]

  - _parse_target() used !defined $arg3 to detect the single-argument
    shorthand form ('Pkg::method').  Because no caller ever passes three
    arguments to _parse_target(), $arg3 is always undef, making the guard
    permanently true.  As a result, spy('A::B', 'method') was misread as
    shorthand and resolved to package 'A', method 'B' -- silently spying
    on the wrong target.  Fixed by changing the discriminator to
    !defined $arg2: the shorthand form has exactly one argument (arg2
    undef), the longhand form has two (arg2 defined).  All other callers
    pass a single string argument and are unaffected.

  - mock() emitted "Prototype mismatch: sub ... ()" warnings whenever it
    replaced a function that carried a Perl prototype (such as the ()
    no-args prototype used by I18N::LangTags::Detect::detect).  The
    warning fired because the replacement coderef had no prototype, so
    Perl flagged the redefinition as a signature mismatch.  Fixed by
    calling Scalar::Util::set_prototype on the replacement immediately
    before installing it, copying the prototype from the original coderef.
    The call uses the & sigil (&Scalar::Util::set_prototype) to bypass
    set_prototype's own (&$) prototype constraint, which would otherwise
    reject a lexical variable as the first argument.  unmock() required
    no change: reinstating the original coderef via glob assignment
    restores its prototype automatically.

  - Scalar::Util is now loaded (use Scalar::Util ()) without importing
    set_prototype into the Test::Mockingbird namespace, avoiding any
    risk of the imported alias inheriting the (&$) prototype constraint
    at the call site.

  [Notes]

  - spy() installs its wrapper coderef directly without going through
    mock(), so it does not benefit from the set_prototype fix and still
    emits a prototype-mismatch warning when wrapping a prototyped
    function.  This is a known limitation documented in the test suite
    and will be addressed separately.

  [Tests]

  - Added prototype-preservation subtests to all four test files:

    unit.t        Six white-box subtests confirming that prototype()
                  on the installed glob matches the original after
                  mock() for (), ($$), ($), and no-prototype functions,
                  including across stacked mocks and after unmock.

    function.t    Three subtests capturing $SIG{__WARN__} and asserting
                  no prototype-mismatch warning is emitted during the
                  mock/unmock cycle for (), ($$), and no-prototype
                  functions.  Return-value assertions on () functions
                  use ->can() to bypass Perl's compile-time constant
                  inlining of () prototype functions.

    integration.t Three end-to-end subtests covering plain mock(),
                  deep_mock(), and mock_scoped() on a () prototype
                  function.  All use ->can() closures for return-value
                  assertions; the plain mock() subtest uses the
                  fully-qualified Test::Mockingbird::restore_all() to
                  avoid shadowing by the TimeTravel restore_all import.

    edge-cases.t  Five boundary-condition subtests: () warning
                  suppression; stacked mocks each independently carrying
                  the prototype; mock_scoped() delegating correctly to
                  mock(); spy() documented as a known limitation that
                  still emits the mismatch warning; and ($$) full cycle.

0.09	Mon May  4 20:22:49 EDT 2026

  Bug fixes

  - mock_scoped was recording two diagnostic meta layers per call: one of
    type 'mock' (emitted by the internal mock() call) and a second of type
    'mock_scoped' (pushed explicitly afterwards).  diagnose_mocks() therefore
    reported depth 2 and a misleading 'mock' entry for every mock_scoped
    installation.  Fixed by setting local $TYPE = 'mock_scoped' before
    delegating to mock(), matching the pattern already used by mock_return,
    mock_exception, mock_sequence, and mock_once.  Each mock_scoped call now
    records exactly one layer of the correct type.

  New features

  - mock_scoped now accepts multiple method/coderef pairs in a single call,
    returning one guard that restores all of them on destruction.  Four
    argument forms are supported:

      Single shorthand (unchanged):
        my $g = mock_scoped 'Pkg::method' => sub { ... };

      Single longhand (unchanged):
        my $g = mock_scoped('Pkg', 'method', sub { ... });

      Multi shorthand -- pairs of fully-qualified-name, coderef:
        my $g = mock_scoped(
            'Pkg::fetch'  => sub { ... },
            'Other::save' => sub { ... },
        );

      Multi longhand -- package followed by method/coderef pairs:
        my $g = mock_scoped('Pkg',
            fetch  => sub { ... },
            save   => sub { ... },
            remove => sub { ... },
        );

    All methods covered by a multi-method guard are restored atomically when
    the guard goes out of scope or is explicitly undefed.

  - Test::Mockingbird::Guard updated to store a list of fully-qualified
    method names rather than a single name, enabling the multi-method
    mock_scoped forms above.  Single-method behaviour is unchanged.

  Tests

  - Added t/mock_scoped_multi.t (39 assertions) covering: the meta-layer
    bug fix; no-regression checks on both single-method forms; all three
    new multi-method forms (multi shorthand, multi longhand two methods,
    multi longhand three methods); explicit guard undef; and
    diagnose_mocks() state before and after guard destruction.

0.08	Tue Apr  7 18:49:17 EDT 2026
	Test::Mockingbird::DESTROY now calls restore_all

0.07	Mon Mar 23 20:15:20 EDT 2026
	- Added Test::Mockingbird::TimeTravel:
		* Integration to Test::Mockingbird::DeepMock.
		* New 'now' plan supporting freeze, travel, advance, rewind.
		* Automatic restoration of time state after deep_mock block.
		* Deterministic interaction between mocks, spies, and frozen time.
		* Added integration test exercising mixed mocking + time travel.

0.06	Fri Mar 20 08:13:40 EDT 2026
	- Add restore(): restore all mock layers for a single method target.
	- Added diagnose_mocks() and diagnose_mocks_pretty() for structured and
		human-readable inspection of active mock layers,
		including type and installation location.

0.05	Thu Mar 19 19:05:22 EDT 2026
	- DeepMock:
		- Added args_eq and args_deeply expectation types for exact and deep argument matching
		- Added never expectation type to assert that a spy was not called
	- Meets PBP level 5
	- Disallow setting a mock to undef
	- Add mock_return, mock_exception, and mock_sequence sugar helpers for common mocking patterns.
	- Add mock_once: a one-shot mock that restores itself after the first call.

0.04	Thu Mar 19 08:36:23 EDT 2026
	- Refactored unmock() to reliably support both shorthand and longhand targets.
	- Added DeepMock

0.03	Wed Mar	4 07:29:58 EST 2026
	Added shorthand syntax
	Added mock_scoped

0.02	Thu Jan	9 08:05:34 EST 2025
	Updated spy.t to check that the spied routine is called,
		and spying is stopped after restore
	More tests

0.01	Wed Jan	8 13:22:13 EST 2025
	First draft
