File Coverage

blib/lib/MooseX/Test/Role.pm
Criterion Covered Total %
statement 35 98 35.7
branch 1 34 2.9
condition 1 11 9.0
subroutine 13 21 61.9
pod 4 5 80.0
total 54 169 31.9


line stmt bran cond sub pod time code
1             package MooseX::Test::Role;
2              
3             our $VERSION = '0.06';
4              
5 6     6   308364 use strict;
  6         16  
  6         224  
6 6     6   34 use warnings;
  6         11  
  6         187  
7              
8 6     6   44 use Carp qw( confess );
  6         11  
  6         564  
9 6     6   2416 use Class::Load qw( try_load_class );
  6         93566  
  6         303  
10 6     6   38 use List::Util qw( first );
  6         13  
  6         673  
11 6     6   34 use Test::Builder;
  6         11  
  6         172  
12              
13 6     6   36 use Exporter qw( import unimport );
  6         11  
  6         5723  
14             our @EXPORT = qw( requires_ok consumer_of consuming_object consuming_class );
15              
16             sub requires_ok {
17 1     1 1 34749 my ( $role, @required ) = @_;
18 1         5 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 2     2 1 28537 my ( $role, %args ) = @_;
37              
38 2 50       12 my %methods = exists $args{methods} ? %{ $args{methods} } : ();
  0         0  
39              
40 2         11 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 28794 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 27597 my ( $role, %methods ) = @_;
70              
71 1 0       5 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 4     4   9 my $role = shift;
95              
96 4 0 33     71 if ($role->can('meta') && $role->meta()->isa('Moose::Meta::Role')) {
97             # Also covers newer Moo::Roles
98 0           return 'Moose::Role';
99             }
100              
101 0 0 0       if (try_load_class('Role::Tiny') && _role_tiny_info($role)) {
102             # Also covers older Moo::Roles
103 0           return 'Role::Tiny';
104             }
105              
106 0           return;
107             }
108              
109             my $package_counter = 0;
110             sub _package_name {
111 0     0     return 'MooseX::Test::Role::Consumer' . $package_counter++;
112             }
113              
114             sub _apply_role {
115 0     0     my %args = @_;
116              
117 0           my $package = $args{package};
118 0           my $role_type = $args{role_type};
119 0           my $role = $args{role};
120              
121             # We'll need a thing that exports a "with" sub
122 0           my $with_exporter;
123 0 0         if ($role_type eq 'Moose::Role') {
    0          
124 0           $with_exporter = 'Moose';
125             }
126             elsif ($role_type eq 'Role::Tiny') {
127 0           $with_exporter = 'Role::Tiny::With';
128             }
129             else {
130 0           confess "Unknown role type $role_type";
131             }
132              
133 0           my $source = qq{
134             package $package;
135              
136             use $with_exporter;
137             with('$role');
138             };
139              
140             #warn $source;
141              
142 0           eval($source);
143 0 0         die $@ if $@;
144              
145 0           return $package;
146             }
147              
148             sub _add_methods {
149 0     0     my %args = @_;
150              
151 0           my $role_type = $args{role_type};
152 0           my $package = $args{package};
153 0           my $role = $args{role};
154 0           my $methods = $args{methods};
155              
156 0   0 0     $methods->{$_} ||= sub { undef } for _required_methods( $role_type, $role );
  0            
157              
158 0           my $meta;
159 0 0         $meta = Moose::Meta::Class->create($package) if $role_type eq 'Moose::Role';
160              
161 0           while ( my ( $method, $subref ) = each(%{$methods}) ) {
  0            
162 0 0         if ($meta) {
163 0           $meta->add_method($method => $subref);
164             }
165             else {
166 6     6   36 no strict 'refs';
  6         11  
  6         1325  
167             #no warnings 'redefine';
168 0           *{ $package . '::' . $method } = $subref;
  0            
169             }
170             }
171              
172 0           return;
173             }
174              
175             sub _role_tiny_info {
176             # This seems brittle, but there aren't many options to get this data.
177             # Moo relies on %INFO too, so it seems like it would be a hard thing
178             # for to move away from.
179              
180 0     0     my $role = shift;
181 0           return $Role::Tiny::INFO{$role};
182             }
183              
184             my $Test = Test::Builder->new();
185              
186             # Done this way for easier testing
187             our $ok = sub { $Test->ok(@_) };
188 0     0 0   sub ok { $ok->(@_) }
189              
190             1;
191              
192             =pod
193              
194             =head1 NAME
195              
196             MooseX::Test::Role - Test functions for Moose roles
197              
198             =head1 SYNOPSIS
199              
200             use MooseX::Test::Role;
201             use Test::More tests => 2;
202              
203             requires_ok('MyRole', qw/method1 method2/);
204              
205             my $consumer = consuming_object(
206             'MyRole',
207             methods => {
208             method1 => sub { 1 }
209             }
210             );
211             ok( $consumer->myrole_method );
212             is( $consumer->method1, 1 );
213              
214             my $consuming_class = consuming_class('MyRole');
215             ok( $consuming_class->class_method() );
216              
217             =head1 DESCRIPTION
218              
219             Provides functions for testing roles. Supports roles created with
220             L, L or L.
221              
222             =head1 BACKGROUND
223              
224             Unit testing a role can be hard. A major problem is creating classes that
225             consume the role.
226              
227             One could side-step the problem entirely and just call the subroutines in the
228             role's package directly. For example,
229              
230             Fooable->bar();
231              
232             That only works until C calls another method in the consuming class
233             though. Mock objects are a tempting way to solve that problem:
234              
235             my $consumer = Test::MockObject->new();
236             $consumer->set_always('baz', 1);
237             Fooable::bar($consumer);
238              
239             But if C happens to call another method in the role then
240             the mock consumer will have to mock that method too.
241              
242             A better way is to create a class to consume the role:
243              
244             package FooableTest;
245              
246             use Moose;
247             with 'Fooable';
248              
249             sub required_method {}
250              
251             package main;
252              
253             my $consumer = FooableTest->new();
254             $consumer->bar();
255              
256             This can work well for some roles. Unfortunately, if several variations have to
257             be tested, it may be necessary to create several consuming test classes, which
258             gets tedious.
259              
260             Moose can create anonymous classes which consume roles:
261              
262             my $consumer = Moose::Meta::Class->create_anon_class(
263             roles => ['Fooable'],
264             methods => {
265             required_method => sub {},
266             }
267             )->new_object();
268             $consumer->bar();
269              
270             This can still be tedious, especially for roles that require lots of methods.
271             C simply makes this easier to do.
272              
273             =head1 EXPORTED FUNCTIONS
274              
275             =over 4
276              
277             =item C \%methods)>
278              
279             Creates a class which consumes the role, and returns it's package name.
280              
281             C<$role> must be the package name of a role. L, L and
282             L are supported.
283              
284             Any method required by the role will be stubbed. To override the default stub
285             methods, or to add additional methods, specify the name and a coderef:
286              
287             consuming_class('MyRole',
288             method1 => sub { 'one' },
289             method2 => sub { 'two' },
290             required_method => sub { 'required' },
291             );
292              
293             =item C \%methods)>
294              
295             Creates a class which consumes the role, and returns an instance of it.
296              
297             If the class does not have a C method (which is commonly the case for
298             L), then the package name will be returned instead.
299              
300             See C for arguments. C is essentially
301             equivalent to:
302              
303             consuming_class(@_)->new();
304              
305             =item C
306              
307             Alias of C, without named arguments. This is left in for
308             compatibility, new code should use C.
309              
310             =item C
311              
312             Tests if role requires one or more methods.
313              
314             =back
315              
316             =head1 GITHUB
317              
318             Patches, comments or mean-spirited code reviews are all welcomed on GitHub:
319              
320             L
321              
322             =head1 AUTHOR
323              
324             Paul Boyd
325              
326             =head1 COPYRIGHT AND LICENSE
327              
328             This software is copyright (c) 2014 by Paul Boyd.
329              
330             This is free software; you can redistribute it and/or modify it under
331             the same terms as the Perl 5 programming language system itself.
332              
333             =cut