I have a Moose class that needs to send requests of type Foo::Request
. I need to make this dependency accessible from the outside, so that I can easily exchange the request implementation in tests. I came up with the following attribute:
has request_builder => (
is => 'rw',
isa => 'CodeRef',
default => sub {
sub { Foo::Request->new(@_) }
}
);
And then in code:
my $self = shift;
my $request = $self->request_builder->(path => …);
And in tests:
my $tested_class = …;
my $request = Test::MockObject->new;
$request->mock(…);
$tested_class->request_builder(sub { $request });
Is there a more simple / more idiomatic solution?
How about applying a role dynamically in your tests with Moose::Util::apply_all_roles? I have been wanting to use this for a while, but haven't had an excuse yet. Here is how I think it would work.
First, modify your original attribute slightly:
package MyClientThing;
has request => (
is => 'rw',
isa => 'Foo::Request',
builder => '_build_request',
);
sub _build_request { Foo::Request->new };
....
Then create a Test::RequestBuilder role:
package Test::RequestBuilder;
use Moose::Role;
use Test::Foo::Request; # this module could inherit from Foo::Request I guess?
sub _build_request { return Test::Foo::Request->new };
Meanwhile in 't/my_client_thing.t' you would write something like this:
use MyClientThing;
use Moose::Util qw( apply_all_roles );
use Test::More;
my $client = MyClientThing->new;
apply_all_roles( $client, 'Test::RequestBuilder' );
isa_ok $client->request, 'Test::Foo::Request';
See Moose::Manual::Roles for more info.
My suggestion, following the model in chromatic's article (comment above by Mike), is this:
In your class:
has request => (
is => 'ro',
isa => 'CodeRef',
default => sub {
Foo::Request->new(@_)
}
);
In your test:
my $request = Test::MockObject->new;
$request->mock(…);
my $tested_class = MyClass->new(request => $request, ...);
Does exactly what your code does, with the following refinements:
request
attribute is a ready-to-use object; no need to dereference the sub refConsider this approach:
In your Moose class define an 'abstract' method called make_request
. Then define two roles which implement make_request
- one which calls Foo::Request->new
and another one which calls Test::MockObject->new
.
Example:
Your main class and the two roles:
package MainMooseClass;
use Moose;
...
# Note: this class requires a role that
# provides an implementation of 'make_request'
package MakeRequestWithFoo;
use Moose::Role;
use Foo::Request; # or require it
sub make_request { Foo::Request->new(...) }
package MakeRequestWithMock;
use Moose::Role;
use Test::MockRequest; # or require it
sub make_request { Test::MockRequest->new(...) }
If you want to test your main class, mix it with the 'MakeRequestWithMock' role:
package TestVersionOfMainMooseClass;
use Moose;
extends 'MainMooseClass';
with 'MakeRequestWithMock';
package main;
my $test_object = TestVersionOfMainMooseClass->new(...);
If you want to use it with the Foo implementation of 'make_request', mix it in with the 'MakeRequestWithFoo' role.
Some advantages:
You will only load in modules that you need. For instance, the class TestVersionOfMainMooseClass
will not load the module Foo::Request
.
You can add data that is relevant/required by your implementation of make_request
as instance members of your new class. For example, your original approach of using a CODEREF can be implemented with this role:
package MakeRequestWithCodeRef;
use Moose::Role;
has request_builder => (
is => 'rw',
isa => 'CodeRef',
required => 1,
);
sub make_request { my $self = shift; $self->request_builder->(@_) };
To use this class you need to supply an initializer for request_builder
, e.g.:
package Example;
use Moose;
extends 'MainMooseClass';
with 'MakeRequestWithCodeRef';
package main;
my $object = Example->new(request_builder => sub { ... });
As a final consideration, the roles you write might be usable with other classes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With