Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/testsuite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,60 @@ jobs:
- run: perl Build.PL
- run: ./Build
- run: ./Build test

# Exercises every optional-OO-dependency permutation -- the full
# {Moo, Moose, Mouse} cross-product (8 profiles) -- so each OO system is
# tested in isolation and in combination. Uses a clean setup-perl Perl
# (NOT the perl-tester container, which preloads Moose and would defeat the
# `none`/`moo`/`mouse` profiles) and installs each profile's modules
# explicitly (no --with-recommends). The `moo` profile -- Moo present, Moose
# absent -- is the configuration that reproduces GH #93. Support deps are
# paired with the systems whose tests need them: Class::Load + Test::Exception
# for the Moose tests (t/moose_around.t needs Class::Load), Test::Exception
# for t/mouse_basic.t. Tests for an absent system skip_all, so a profile only
# installs what it actually exercises.
oo-deps:
name: oo-deps (${{ matrix.profile.name }})
env:
PERL_USE_UNSAFE_INC: 0
AUTOMATED_TESTING: 1

runs-on: ubuntu-latest

needs: [ubuntu]

strategy:
fail-fast: false
matrix:
profile:
- name: none
install: ""
- name: moo
install: "Moo"
- name: moose
install: "Moose Class::Load Test::Exception"
- name: mouse
install: "Mouse Test::Exception"
- name: moo+moose
install: "Moo Moose Class::Load Test::Exception"
- name: moo+mouse
install: "Moo Mouse Test::Exception"
- name: moose+mouse
install: "Moose Mouse Class::Load Test::Exception"
- name: all
install: "Moo Moose Mouse Class::Load Test::Exception"

steps:
- uses: actions/checkout@v6
- uses: shogo82148/actions-setup-perl@v1
- run: perl -V
# Required deps only -- NO --with-recommends, so the optional OO modules
# are controlled per-profile below.
- name: install required dependencies
run: cpanm --installdeps --notest .
- name: install OO profile dependencies
if: matrix.profile.install != ''
run: cpanm --notest ${{ matrix.profile.install }}
- run: perl Build.PL
- run: ./Build
- run: ./Build test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ pm_to_blib

Test-MockModule-*
docs/superpowers
.tickets/
6 changes: 4 additions & 2 deletions Build.PL
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ my $builder = $class->new(
'Test::Warnings' => 0,
},
# Moose, Mouse, Class::Load and Test::Exception are exercised ONLY by the
# t/moose_*.t and t/mouse_*.t MOP-integration tests added for GH #55. Each
# of those files opens with `eval { require Moose } or plan skip_all`, so
# t/moose_*.t and t/mouse_*.t MOP-integration tests added for GH #55, and
# Moo only by the t/moo_*.t tests added for GH #93. Each of those files
# opens with an `eval { require <Module> } or plan skip_all`, so
# the suite passes cleanly without them. They are therefore OPTIONAL, not
# required: declaring them as `recommends` keeps a plain
# `cpanm Test::MockModule` from dragging in two full Perl OO systems (GH #90).
Expand All @@ -65,6 +66,7 @@ my $builder = $class->new(
'Class::Load' => 0,
'Moose' => 0,
'Mouse' => 0,
'Moo' => 0,
},

add_to_cleanup => [
Expand Down
5 changes: 5 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Unreleased

## What's Changed
* fix(GH #93): don't require Moose when mocking a Moo class — `_meta_for` no longer triggers Moo's lazy Moose metaclass inflation

## 0.185.2 — 2026-05-29

## What's Changed
Expand Down
5 changes: 5 additions & 0 deletions lib/Test/MockModule.pm
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,11 @@ sub _meta_for {
return unless $package->can('meta');
my $meta = eval { $package->meta };
return unless ref $meta;
# Moo's ->meta returns a Moo::HandleMoose::FakeMetaClass whose ->isa lazily
# inflates a real Moose metaclass -- which throws when Moose isn't installed.
# Match it by exact ref (not ->isa) BEFORE the isa checks so inflation never
# fires; Moo classes fall back to plain symbol-table mocking. (GH #93)
return if ref($meta) eq 'Moo::HandleMoose::FakeMetaClass';
return $meta if $meta->isa('Class::MOP::Class'); # Moose
return $meta if $meta->isa('Mouse::Meta::Class'); # Mouse
return;
Expand Down
52 changes: 52 additions & 0 deletions t/moo_basic.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use strict;
use warnings;
use Test::More;

BEGIN {
eval { require Moo; 1 } or plan skip_all => "Moo not installed";
}

use Test::MockModule;

{
package Issue93::MooClass; ## no critic (Modules::RequireFilenameMatchesPackage)
use Moo;
has answer => (is => 'ro', default => 42);
sub greet { 'real_greet' }
}

{
package Issue93::MooParent; ## no critic (Modules::RequireFilenameMatchesPackage)
use Moo;
sub bar { 'parent_bar' }
}
{
package Issue93::MooChild; ## no critic (Modules::RequireFilenameMatchesPackage)
use Moo;
extends 'Issue93::MooParent';
}

# Basic mock-and-call
my $mock = Test::MockModule->new('Issue93::MooClass');
$mock->mock( greet => sub { 'mocked_greet' } );
is(Issue93::MooClass->new->greet, 'mocked_greet', "Moo mock visible");
is(Issue93::MooClass->new->answer, 42, "Moo accessor unaffected by mock");

$mock->unmock('greet');
is(Issue93::MooClass->new->greet, 'real_greet', "Moo unmock restores original");

# Moo-generated accessors are ordinary subs in the symbol table, so plain
# replacement mocking must work on them too.
$mock->mock( answer => sub { 99 } );
is(Issue93::MooClass->new->answer, 99, "Moo accessor mock visible");
$mock->unmock('answer');
is(Issue93::MooClass->new->answer, 42, "Moo accessor unmock restores original");

# Inherited method mock-and-restore
my $imock = Test::MockModule->new('Issue93::MooChild');
$imock->mock( bar => sub { 'mocked_bar' } );
is(Issue93::MooChild->bar, 'mocked_bar', "Moo inherited mock visible");
$imock->unmock('bar');
is(Issue93::MooChild->bar, 'parent_bar', "Moo unmock falls through to parent");

done_testing;
36 changes: 36 additions & 0 deletions t/moo_meta_for.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use strict;
use warnings;
use Test::More;

BEGIN {
eval { require Moo; 1 } or plan skip_all => "Moo not installed";
}

use Test::MockModule;

# Plain (non-Moo) package
{
package PlainPkg; ## no critic (Modules::RequireFilenameMatchesPackage)
sub new { bless {}, shift }
}

# Moo package
{
package MooPkg; ## no critic (Modules::RequireFilenameMatchesPackage)
use Moo;
has answer => (is => 'ro', default => 42);
sub greet { 'real_greet' }
}

# A Moo class must NOT be treated as a Moose/Mouse MOP class: _meta_for
# returns undef so mocking falls back to plain symbol-table replacement.
# Pre-fix this either returns a truthy Moo::HandleMoose::FakeMetaClass
# (Moose installed) or throws inside Moo::HandleMoose (Moose absent).
is(Test::MockModule::_meta_for('MooPkg'), undef, "Moo package returns undef meta");

is(Test::MockModule::_meta_for('PlainPkg'), undef, "plain package returns undef");
is(Test::MockModule::_meta_for(undef), undef, "undef package returns undef");
is(Test::MockModule::_meta_for(''), undef, "empty package returns undef");
is(Test::MockModule::_meta_for('Does::Not::Exist'), undef, "nonexistent package returns undef");

done_testing;