perlunit-testingmoosemoo

How do I mock a method defined in a Moo Role?


Given the following Role:

package MyRole;
use Moo::Role;

sub foo { 
    return 'blah';
}

And the following consuming class:

package MyClass;
use Moo;
with 'MyRole';

around foo = sub { 
    my ($orig, $self) = @_;
    return 'bak' if $self->$orig eq 'baz';
    return $self->$orig;
}

I would like to test behaviour defined in the around modifier. How do I do this? It seems that Test::MockModule won't work:

use MyClass;
use Test::Most;
use Test::MockModule;

my $mock = Test::MockModule->new('MyRole');
$mock->mock('foo' => sub { return 'baz' });

my $obj = MyClass->new;
# Does not work
is $obj->foo, 'bak', 'Foo is what it oughtta be';

EDIT: What I'm looking to test is the interaction of MyClass with MyRole as defined in the around modifier. I want to test that the code in the around modifier does what I think it should. Here's another example that's closer to my actual code:

package MyRole2
use Moo::Role;

sub call {
    my $self = shift;
    # Connect to server, retrieve a document
    my $document = $self->get_document;
    return $document;
}

package MyClass2;
use Moo;
with 'MyRole2';

around call = sub { 
    my ($orig, $self) = @_;
    my $document = $self->$orig;
    if (has_error($document)) {
        die 'Error';
    }
    return parse($document);
};

So what I want to do here is to mock MyRole2::call to return a static document, defined in my test fixtures, that contains errors and test that the exception is thrown properly. I know how to test it using Test::More::throws_ok or similar. What I don't know how to do is to mock MyRole2::call and not MyClass2::call.


Solution

  • From mst on #moose:

    use 5.016;
    use Test::Most tests => 1;
    
    require MyRole;
    
    our $orig = MyRole->can('foo');
    no warnings 'redefine';
    *MyRole::foo = sub { goto &$orig };
    
    {
        local $orig = sub {'baz'};
        require MyClass;
        my $obj = MyClass->new;
        is $obj->foo, 'bak', 'Foo is what it oughtta be'; 
    }
    

    The trick is to override MyRole::foo before anything that uses it gets loaded. Which means using require MyClass instead of use MyClass, because use MyClass translates to BEGIN { require MyClass } which defeats the whole thing of overriding the method before anything using it gets loaded.