File Coverage

blib/lib/MooseX/Test/Role.pm
Criterion Covered Total %
statement 36 103 34.9
branch 1 36 2.7
condition 1 11 9.0
subroutine 13 22 59.0
pod 4 5 80.0
total 55 177 31.0


line stmt bran cond sub pod time code
1             package MooseX::Test::Role;
2              
3             our $VERSION = '0.07';
4              
5 7     7   258382 use strict;
  7         15  
  7         272  
6 7     7   34 use warnings;
  7         14  
  7         220  
7              
8 7     7   42 use Carp qw( confess );
  7         10  
  7         422  
9 7     7   1110 use Class::Load qw( try_load_class );
  7         47049  
  7         345  
10 7     7   44 use List::Util qw( first );
  7         11  
  7         660  
11 7     7   40 use Test::Builder;
  7         11  
  7         204  
12              
13 7     7   42 use Exporter qw( import unimport );
  7         7  
  7         5650  
14             our @EXPORT = qw( requires_ok consumer_of consuming_object consuming_class );
15              
16             sub requires_ok {
17 1     1 1 20662 my ( $role, @required ) = @_;
18 1         6 my $msg = "$role requires " . join( ', ', @required );
19              
20 1         4 my $role_type = _derive_role_type($role);
21 0 0       0 if (!$role_type) {
22 0         0 ok( 0, $msg );
23 0         0 return;
24             }
25              
26 0         0 foreach my $req (@required) {
27 0 0   0   0 unless ( first { $_ eq $req } _required_methods($role_type, $role) ) {
  0         0  
28 0         0 ok( 0, $msg );
29 0         0 return;
30             }
31             }
32 0         0 ok( 1, $msg );
33             }
34              
35             sub consuming_class {
36 3     3 1 35219 my ( $role, %args ) = @_;
37              
38 3 50       16 my %methods = exists $args{methods} ? %{ $args{methods} } : ();
  0         0  
39              
40 3         12 my $role_type = _derive_role_type($role);
41 0 0       0 confess 'first argument should be a role' unless $role_type;
42              
43 0         0 my $package = _package_name();
44 0         0 _add_methods(
45             package => $package,
46             role_type => $role_type,
47             role => $role,
48             methods => \%methods,
49             );
50              
51 0         0 _apply_role(
52             package => $package,
53             role_type => $role_type,
54             role => $role,
55             );
56              
57 0         0 return $package;
58             }
59              
60             sub consuming_object {
61 1     1 1 21214 my $class = consuming_class(@_);
62              
63             # Moose and Moo can be instantiated and should be. Role::Tiny however isn't
64             # a full OO implementation and so doesn't provide a "new" method.
65 0 0       0 return $class->can('new') ? $class->new() : $class;
66             }
67              
68             sub consumer_of {
69 1     1 1 21637 my ( $role, %methods ) = @_;
70              
71 1 0       7 confess 'first argument to consumer_of should be a role' unless _derive_role_type($role);
72              
73 0         0 return consuming_object( $role, methods => \%methods );
74             }
75              
76             sub _required_methods {
77 0     0   0 my ($role_type, $role) = @_;
78 0         0 my @methods;
79              
80 0 0       0 if ($role_type eq 'Moose::Role') {
    0          
81 0         0 @methods = $role->meta->get_required_method_list();
82             }
83             elsif ($role_type eq 'Role::Tiny') {
84 0         0 my $info = _role_tiny_info($role);
85 0 0 0     0 if ($info && ref($info->{requires}) eq 'ARRAY') {
86 0         0 @methods = @{$info->{requires}};
  0         0  
87             }
88             }
89              
90 0 0       0 return wantarray ? @methods : \@methods;
91             }
92              
93             sub _derive_role_type {
94 5     5   10 my $role = shift;
95              
96 5         23 try_load_class($role);
97              
98 5 0 33     1421 if ($role->can('meta') && $role->meta()->isa('Moose::Meta::Role')) {
99             # Also covers newer Moo::Roles
100 0           return 'Moose::Role';
101             }
102              
103 0 0 0       if (try_load_class('Role::Tiny') && _role_tiny_info($role)) {
104             # Also covers older Moo::Roles
105 0           return 'Role::Tiny';
106             }
107              
108 0           return;
109             }
110              
111             my $package_counter = 0;
112             sub _package_name {
113 0     0     return 'MooseX::Test::Role::Consumer' . $package_counter++;
114             }
115              
116             sub _apply_role {
117 0     0     my %args = @_;
118              
119 0           my $package = $args{package};
120 0           my $role_type = $args{role_type};
121 0           my $role = $args{role};
122              
123             # We'll need a thing that exports a "with" sub
124 0           my $with_exporter;
125 0 0         if ($role_type eq 'Moose::Role') {
    0          
126 0           $with_exporter = 'Moose';
127             }
128             elsif ($role_type eq 'Role::Tiny') {
129 0           $with_exporter = 'Role::Tiny::With';
130             }
131             else {
132 0           confess "Unknown role type $role_type";
133             }
134              
135 0           my $source = qq{
136             package $package;
137              
138             use $with_exporter;
139             with('$role');
140             };
141              
142             #warn $source;
143              
144 0           eval($source);
145 0 0         die $@ if $@;
146              
147 0           return $package;
148             }
149              
150             sub _add_methods {
151 0     0     my %args = @_;
152              
153 0           my $role_type = $args{role_type};
154 0           my $package = $args{package};
155 0           my $role = $args{role};
156 0           my $methods = $args{methods};
157              
158 0   0 0     $methods->{$_} ||= sub { undef } for _required_methods( $role_type, $role );
  0            
159              
160 0           my $meta;
161 0 0         $meta = Moose::Meta::Class->create($package) if $role_type eq 'Moose::Role';
162              
163 0           while ( my ( $method, $subref ) = each(%{$methods}) ) {
  0            
164             # This allows us to have scalar values without an anonymous sub in the
165             # definition, similar to Moose's default values. We need to make a lexical
166             # copy of $subref so we do not build a circle where we return itself as a
167             # coderef later on.
168 0 0         if ( !ref $subref ) {
169 0           my $value = $subref;
170 0     0     $subref = sub { $value };
  0            
171             }
172              
173 0 0         if ($meta) {
174 0           $meta->add_method($method => $subref);
175             }
176             else {
177 7     7   43 no strict 'refs';
  7         9  
  7         1315  
178             #no warnings 'redefine';
179 0           *{ $package . '::' . $method } = $subref;
  0            
180             }
181             }
182              
183 0           return;
184             }
185              
186             sub _role_tiny_info {
187             # This seems brittle, but there aren't many options to get this data.
188             # Moo relies on %INFO too, so it seems like it would be a hard thing
189             # for to move away from.
190              
191 0     0     my $role = shift;
192 0           return $Role::Tiny::INFO{$role};
193             }
194              
195             my $Test = Test::Builder->new();
196              
197             # Done this way for easier testing
198             our $ok = sub { $Test->ok(@_) };
199 0     0 0   sub ok { $ok->(@_) }
200              
201             1;
202              
203             =pod
204              
205             =head1 NAME
206              
207             MooseX::Test::Role - Test functions for Moose roles
208              
209             =head1 SYNOPSIS
210              
211             use MooseX::Test::Role;
212             use Test::More tests => 3;
213              
214             requires_ok('MyRole', qw/method1 method2/);
215              
216             my $consumer = consuming_object(
217             'MyRole',
218             methods => {
219             method1 => 1,
220             method2 => sub { shift->method1() },
221             }
222             );
223             ok( $consumer->myrole_method );
224             is( $consumer->method1, 1 );
225             is( $consumer->method2, 1 );
226              
227             my $consuming_class = consuming_class('MyRole');
228             ok( $consuming_class->class_method() );
229              
230             =head1 DESCRIPTION
231              
232             Provides functions for testing roles. Supports roles created with
233             L, L or L.
234              
235             =head1 BACKGROUND
236              
237             Unit testing a role can be hard. A major problem is creating classes that
238             consume the role.
239              
240             One could side-step the problem entirely and just call the subroutines in the
241             role's package directly. For example,
242              
243             Fooable->bar();
244              
245             That only works until C calls another method in the consuming class
246             though. Mock objects are a tempting way to solve that problem:
247              
248             my $consumer = Test::MockObject->new();
249             $consumer->set_always('baz', 1);
250             Fooable::bar($consumer);
251              
252             But if C happens to call another method in the role then
253             the mock consumer will have to mock that method too.
254              
255             A better way is to create a class to consume the role:
256              
257             package FooableTest;
258              
259             use Moose;
260             with 'Fooable';
261              
262             sub required_method {}
263              
264             package main;
265              
266             my $consumer = FooableTest->new();
267             $consumer->bar();
268              
269             This can work well for some roles. Unfortunately, if several variations have to
270             be tested, it may be necessary to create several consuming test classes, which
271             gets tedious.
272              
273             Moose can create anonymous classes which consume roles:
274              
275             my $consumer = Moose::Meta::Class->create_anon_class(
276             roles => ['Fooable'],
277             methods => {
278             required_method => sub {},
279             }
280             )->new_object();
281             $consumer->bar();
282              
283             This can still be tedious, especially for roles that require lots of methods.
284             C simply makes this easier to do.
285              
286             =head1 EXPORTED FUNCTIONS
287              
288             =over 4
289              
290             =item C \%methods)>
291              
292             Creates a class which consumes the role, and returns it's package name.
293              
294             C<$role> must be the package name of a role. L, L and
295             L are supported.
296              
297             Any method required by the role will be stubbed. To override the default stub
298             methods, or to add additional methods, specify the name and a coderef:
299              
300             consuming_class('MyRole',
301             method1 => sub { 'one' },
302             method2 => sub { 'two' },
303             required_method => sub { 'required' },
304             );
305              
306             For methods that should just return a fixed scalar value, you can ommit the
307             coderef.
308              
309             consuming_class('MyRole',
310             method1 => 'one',
311             method_uc1 => sub {
312             my ($self) = @_;
313             lc $self->one;
314             },
315             );
316              
317             =item C \%methods)>
318              
319             Creates a class which consumes the role, and returns an instance of it.
320              
321             If the class does not have a C method (which is commonly the case for
322             L), then the package name will be returned instead.
323              
324             See C for arguments. C is essentially
325             equivalent to:
326              
327             consuming_class(@_)->new();
328              
329             =item C
330              
331             Alias of C, without named arguments. This is left in for
332             compatibility, new code should use C.
333              
334             =item C
335              
336             Tests if role requires one or more methods.
337              
338             =back
339              
340             =head1 GITHUB
341              
342             Patches, comments or mean-spirited code reviews are all welcomed on GitHub:
343              
344             L
345              
346             =head1 AUTHOR
347              
348             Paul Boyd
349              
350             =head1 COPYRIGHT AND LICENSE
351              
352             This software is copyright (c) 2014 by Paul Boyd.
353              
354             This is free software; you can redistribute it and/or modify it under
355             the same terms as the Perl 5 programming language system itself.
356              
357             =cut