File Coverage

blib/lib/Test/Shadow.pm
Criterion Covered Total %
statement 74 74 100.0
branch 30 32 93.7
condition 2 3 66.6
subroutine 14 14 100.0
pod 2 4 50.0
total 122 127 96.0


line stmt bran cond sub pod time code
1             package Test::Shadow;
2              
3 2     2   87415 use strict; use warnings;
  2     2   6  
  2         65  
  2         11  
  2         4  
  2         56  
4              
5 2     2   1679 use parent 'Test::Builder::Module';
  2         588  
  2         11  
6 2     2   1919 use Test::Deep::NoTest qw(deep_diag cmp_details);
  2         316  
  2         12  
7 2     2   216 use Scalar::Util 'reftype';
  2         4  
  2         345  
8              
9             our @EXPORT = qw( with_shadow );
10             our @EXPORT_OK = qw( iterate );
11             our $VERSION = 0.0201;
12              
13             =head1 NAME
14              
15             Test::Shadow - override a class's methods in a scope, checking input/output
16              
17             =head1 SYNOPSIS
18              
19             Provides RSpec-like mocking with 'receive'/'and_return' functionality. However
20             the interface is more explicit. This may be considered a feature.
21              
22             use Test::More;
23             use Test::Shadow;
24              
25             use Foo;
26              
27             with_shadow Foo => inner_method => {
28             in => [ 'list', 'of', 'parameters' ],
29             out => 'barry',
30             count => 3
31             }, sub {
32             my $foo = Foo->new;
33             $foo->outer_method();
34             };
35              
36             =head1 EXPORTED FUNCTIONS
37              
38             =head2 with_shadow
39              
40             Exported by default
41              
42             with_shadow $class1 => $method1 => $args1, ..., $callback;
43              
44             Each supplied class/method is overridden as per the specification in the
45             supplied args. Finally, the callback is run with that specification.
46              
47             The args passed are as follows:
48              
49             =over 4
50              
51             =item in
52              
53             A list of parameters to compare every call of the method against. This will be
54             checked each time, until the first failure, if any. The parameters can be
55             supplied as an arrayref:
56              
57             in => [ 'list', 'of', 'parameters' ]
58              
59             or a hashref:
60              
61             in => { key => 'value', key2 => 'value2 },
62              
63             and the comparison may be made using any of the extended routines in L
64              
65             use Test::Deep;
66             with_shadow Foo => inner_method => {
67             in => { foo => any(1,2,3) },
68             ...
69              
70             =item out
71              
72             Stub the return value. This can be
73              
74             =over 4
75              
76             =item *
77              
78             a simple (non-reference) scalar value
79              
80             ...
81             out => 100,
82              
83             =item *
84              
85             a subroutine ref, which will be passed at every invocation the parameters C<($orig, $self, @args)>.
86              
87             =back
88              
89             Note that the subroutine args are the same as if you were creating a L
90             or L C wrapper, but dynamically scoped to the test.
91              
92             out => sub { my ($orig, $self, @args) = @_; ... },
93              
94             If you want to return a reference (including a subroutine reference) return this from the
95             subroutine: We require wrapping in a subroutine ref for the same reason that Moose's
96             C does: otherwise we would end up passing the same reference to each invocation,
97             with possibly surprising results.
98              
99             out => sub { [] }, # return a new, empty arrayref on each invocation
100              
101             Of course you can simply ignore the call args and invoke as a subroutine. See also
102             the L function.
103              
104             =item count
105              
106             The number of times you expect the method to be called. This is checked at the end
107             of the callback scope.
108              
109             This may be an exact value:
110              
111             count => 4,
112              
113             Or a hashref with one or both of C and C declared:
114              
115             count => { min => 5, max => 10 },
116              
117             =back
118              
119             =head2 iterate
120              
121             We provide a helper function to iterate over a number of scalar return values. This
122             can be attached to C, and takes a list of values to be provided as the stubbed
123             return value on each successive call.
124              
125             use Test::Shadow 'iterate';
126              
127             with_shadow ...
128             out => iterate(1,2,3,4), # return 1 on first invocation, 2 on second, etc.
129             ...
130              
131             The values wrap if they run out: you may want to use a C argument to
132             diagnose that this has happened.
133              
134             As well as simple values, C handles method calls in exactly the same format
135             as they are normally passed to C.
136              
137             with_shadow ...
138             out => iterate(
139             sub { my ($orig, $self, $arg) = @_; ... },
140             ...
141              
142             =cut
143              
144             sub with_shadow {
145 13     13 1 26860 my $sub = pop @_;
146 13         95 my $tb = __PACKAGE__->builder;
147              
148 13         115 my ($class, $method, $shadow_params) = splice @_, 0, 3;
149 13         43 my ($wrapped, $reap) = mk_subs($tb, $class, $method, $shadow_params);
150              
151             {
152 2     2   11 no strict 'refs';
  2         3  
  2         61  
  11         19  
153 2     2   9 no warnings 'redefine';
  2         3  
  2         1585  
154 11         13 local *{"${class}::${method}"} = $wrapped;
  11         40  
155              
156 11 100       30 if (@_) {
157 1         6 with_shadow(@_, $sub);
158             }
159             else {
160 10         30 $sub->();
161             }
162             }
163              
164 11         2340 $reap->();
165             }
166              
167             sub mk_subs {
168 13     13 0 31 my ($tb, $class, $method, $shadow_params) = @_;
169              
170 13 100       124 my $orig = $class->can($method) or die "$class has no such method $method";
171 12         20 my $count = 0;
172 12         15 my $failed;
173              
174 12         24 my $stubbed_out = $shadow_params->{out};
175 12 100       29 if (ref $stubbed_out) {
176 4 100       30 die "out is not a code ref!" unless reftype $stubbed_out eq 'CODE';
177             }
178              
179             my $wrapped = sub {
180 20     20   1071 $count++;
181 20         41 my ($self, @args) = @_;
182              
183 20 100 66     122 if (!$failed and my $expected_in = $shadow_params->{in}) {
184 6 100       25 my $got = (ref $expected_in eq 'HASH') ? { @args } : \@args;
185 6         31 my ($ok, $stack) = cmp_details($got, $expected_in);
186 6 100       41457 if (!$ok) {
187 2         31 $tb->ok(0, sprintf '%s->%s unexpected parameters on call no. %d', $class, $method, $count);
188 2         326 $tb->diag( deep_diag($stack) );
189 2         418 $tb->diag( '(Disabling wrapper)' );
190 2         88 $failed++;
191             }
192             }
193 20 100       42 if ($stubbed_out) {
194             # we use stub even if test has failed, as otherwise we risk calling
195             # mocked service unnecessarily
196              
197 12         32 return stubbed($stubbed_out, $orig, $self, @args);
198             }
199             else {
200 8         33 return $self->$orig(@args);
201             }
202 11         61 };
203             my $reap = sub {
204 11 100   11   67 return if $failed;
205 9 100       28 if (my $expected_in = $shadow_params->{in}) {
206 2         19 $tb->ok(1, "$class->$method parameters as expected");
207             }
208 9 100       1098 if (my $expected_count = $shadow_params->{count}) {
209 3 100       10 if (ref $expected_count) {
210 1 50       5 if (my $min = $expected_count->{min}) {
211 1         6 $tb->ok($count >= $min, "$class->$method call count >= $min");
212             }
213 1 50       380 if (my $max = $expected_count->{max}) {
214 1         8 $tb->ok($count <= $max, "$class->$method call count <= $max");
215             }
216             }
217             else {
218 2         14 $tb->is_num($count, $expected_count,
219             "$class->$method call count as expected ($expected_count)");
220             }
221             }
222 11         55 };
223 11         30 return ($wrapped, $reap);
224             }
225              
226             sub stubbed {
227 20     20 0 39 my ($stubbed_out, $orig, $self, @args) = @_;
228 20 100       39 if (ref $stubbed_out) {
229 12         29 return $stubbed_out->($orig, $self, @args);
230             }
231             else {
232 8         54 return $stubbed_out;
233             }
234             }
235              
236             sub iterate {
237 2     2 1 3158 my @array = my @orig_array = @_;
238             return sub {
239 8     8   11 my ($orig, $self, @args) = @_;
240 8 100       23 @array = @orig_array unless @array;
241 8         21 return stubbed((shift @array), $orig, $self, @args);
242 2         20 };
243             }
244              
245             =head1 SEE ALSO
246              
247             There are several other modules that deal with mocking objects. One of them may well
248             serve your needs better. I was having RSpec envy, about the call expectation side of
249             things (not about the "English-like" DSL, which I found both confusing, and slightly
250             filthy) so Test::Shadow is designed to cover that use case with an API that is less
251             magical and more Perlish (thanks to ribasushi, haarg, tobyink, vincent, ether on
252             #perl-qa for pointing out that my first implementation with the lovely-but-frightening
253             L may not have been the poster child for sanity I'd intended.)
254              
255             =over 4
256              
257             =item *
258              
259             L is the oldest CPAN library I'm aware of. It has a very different
260             usage, where you create an I and stub methods on it, rather
261             than mocking a class.
262              
263             =item *
264              
265             L does mock a class's methods, but hasn't been updated since 2005,
266             and doesn't give the control over return value stubbing and call count tracing.
267              
268             =item *
269              
270             L looks like a more modern mocking implementation. Again, it looks like
271             this works on an object instance.
272              
273             =item *
274              
275             L looks like a good reimplementation of RSpec, which means that
276             personally I dislike aspects of the API -- the monkey-patching and the
277             confusing C and C keywords, but this may be a good choice.
278             Note that the ::Mocks routines are "currently only usable from within tests
279             built with the Test::Spec BDD* framework".
280              
281             =back
282              
283             * my current (snarky) understanding is that "BDD" means something to do with
284             using C and C as synonyms for C.
285              
286             =head1 AUTHOR and LICENSE
287              
288             Copyright 2014 Hakim Cassimally
289              
290             This module is released under the same terms as Perl.
291              
292             =cut
293              
294             1;