File Coverage

blib/lib/Jasmine/Spy.pm
Criterion Covered Total %
statement 300 306 98.0
branch 31 38 81.5
condition 7 15 46.6
subroutine 106 107 99.0
pod 4 4 100.0
total 448 470 95.3


line stmt bran cond sub pod time code
1             package Jasmine::Spy;
2             # ABSTRACT: Mocking library for perl inspired by Jasmine's spies
3             $Jasmine::Spy::VERSION = '1.02';
4             =head1 NAME
5              
6             Jasmine::Spy
7              
8             =head1 VERSION
9              
10             version 1.02
11              
12             =head1 SYNOPSIS
13              
14             use Test::Spec;
15             use Jasmine::Spy qw(spyOn stopSpying expectSpy);
16              
17             describe "FooClass" => sub {
18             before each => sub {
19             spyOn("BarClass", "bazMethod")->andReturn("Bop");
20             spyOn("BarClass", "bam")->andCallThrough;
21             };
22             it "calls BarClass" => sub {
23             FooClass->doTheThing();
24             expectSpy("BarClass", "bazMethod")->toHaveBeenCalled();
25             };
26             it "calls BarClass->bat method with 'bam'" => sub {
27             FooClass->doTheThing();
28             expectSpy("BarClass", "bat")->toHaveBeenCalledWith('bam')
29             };
30             after each => sub {
31             stopSpying("BarClass");
32             };
33             };
34              
35             =head1 Methods
36              
37             Nothing is exported by default, but they cann all be pulled in with the :all tag
38              
39             =head2 Base Class Methods
40              
41             =over 1
42              
43             =item spyOn($invocant, $method)
44              
45             This is the setup method to begin spying. $invocant may be either an object instance or the name of
46             a class. Spying on a Class will automatically spy on all instances of the class, even those created
47             before setting up the spy. Spyng on an instance only effects that instance, not the class or
48             other instances of that class.
49              
50             A "spy" object is returned from this call which will allow introspection and testing of
51             calls. However there is no need to catch this, as other convience methods provide a better
52             way of performing the same introspection later.
53              
54             =item stopSpying($invocant)
55              
56             Use this call to stop spying and restore original functionality to the object or class.
57              
58             =item expectSpy($invocant, $method)
59              
60             Use this to retrieve the "spy" object created by spyOn. It also sets the spy object to
61             introspect of the provided C<$method>. There is only one spy object created for each
62             distinct $invocant beign spied on, even if multiple methods are being watched. This is why
63             C<expectSpy> is the recomended way to start introspection on a spied method.
64              
65             =item getCalls($invocant, $method)
66              
67             This will fetch an array of array's containing the arguments passed each time the C<$method>
68             was called. This is a tied array ref which also provides convience methods C<first> and
69             C<mostRecent>.
70              
71             =back
72              
73             =head2 Spy object methods
74              
75             =over 1
76              
77             =item toHaveBeenCalled
78              
79             Test that the spied method has been called atleast once.
80              
81             =item notToHaveBeenCalled
82              
83             Test that the spied method was never called.
84              
85             =item toHaveBeenCalledWith($matchers)
86              
87             Expects that the spied method has been called with arguments matching C<$matchers> atleast once.
88             This is done with deep comparison via L<Test::Deep>.
89              
90             =item notToHaveBeenCalledWith($matchers)
91              
92             Inverse of toHaveBeenCalledWith.
93              
94             =item andReturn($value)
95              
96             Sets the spied method to return the supplied value. Usually this would be called directly
97             on the return from C<spyOn>.
98              
99             For example:
100              
101             spyOn($foo, 'bar')->andReturn('baz')
102              
103             =item andCallThrough
104              
105             Sets the spied method to call through to the original method, recording arguments passed along
106             the way.
107              
108             =item andCallFake(sub {})
109              
110             Sets the spied method to invoke the supplied code reference in place of the original method.
111             It does also record the arguments along the way.
112              
113             =back
114              
115             =head2 Qunatifiers
116              
117             Quantifiers may be called on the return of either C<toHaveBeenCalled> or C<toHaveBeenCalledWith>
118             to stipulate how many times the method should have been called.
119              
120             =over 1
121              
122             =item once()
123              
124             The method was called exactly one time
125              
126             =item atleast(X)
127              
128             Method was called atleast X times
129              
130             =item atMost(X)
131              
132             Method was not called more than X times
133              
134             =item exactly(X)
135              
136             Method was called exactly X times.
137              
138             =back
139              
140             =head1 TODO
141              
142             =over 1
143              
144             =item Convience Method for andThrow
145              
146             Having put some thought into this, I haven't come up with a clean robust way to handle it.
147             In the end, I think you are better off using C<andCallFake> to throw an exception
148             if you need to test that.
149              
150             =back
151              
152             =head1 See also
153              
154             L<Test::Spec>, L<Test::Deep>
155              
156             =cut
157              
158 1     1   69706 use strict;
  1         1  
  1         23  
159 1     1   3 use warnings;
  1         1  
  1         20  
160 1     1   3 use vars qw(@EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         1  
  1         42  
161 1     1   3 use base qw(Exporter);
  1         1  
  1         55  
162 1     1   495 use Class::MOP;
  1         83106  
  1         65  
163              
164              
165             my (%spies) = ();
166              
167             BEGIN {
168 1     1   2 @EXPORT = ();
169 1         2 @EXPORT_OK = qw(
170             spyOn
171             stopSpying
172             expectSpy
173             getCalls
174             );
175 1         161 %EXPORT_TAGS = (
176             all => \@EXPORT_OK,
177             );
178             }
179              
180             sub spyOn {
181 44     44 1 27286 my ($proto, $method) = @_;
182 44 100       130 if(exists($spies{$proto})){
183 2         9 $spies{$proto}->spyOnMethod($proto, $method);
184             }
185             else {
186 42         93 my $spy = Jasmine::Spy::Instance->new($proto, $method);
187 42         123 $spies{$proto} = $spy;
188             }
189 44         147 return $spies{$proto};
190             }
191              
192             sub stopSpying {
193 23     23 1 8094 my ($proto) = @_;
194 23         31 my $spy = delete $spies{$proto};
195 23 100       48 if($spy){
196 22         37 $spy->stopSpying;
197             }
198             }
199              
200             sub expectSpy {
201 30     30 1 62 my($proto, $method) = @_;
202 30         50 $spies{$proto}->setCurrentMethod($method);
203 30         70 return $spies{$proto};
204             }
205              
206             sub getCalls {
207 8     8 1 20 expectSpy(@_)->calls;
208             }
209              
210             package Jasmine::Spy::Instance;
211             $Jasmine::Spy::Instance::VERSION = '1.02';
212 1     1   6 use warnings;
  1         1  
  1         23  
213 1     1   3 use strict;
  1         1  
  1         19  
214 1     1   3 use base qw(Test::Builder::Module);
  1         1  
  1         61  
215 1     1   12 use Test::Deep;
  1         1  
  1         6  
216 1     1   711 use Want;
  1         1216  
  1         906  
217              
218             sub new {
219 42     42   35 my ($mp, $proto, $method) = @_;
220 42   66     112 my $class = ref($proto) || $proto;
221 1 50   1   485 eval "package $class; use metaclass;" unless ($proto->can("metaclass"));
  1     1   331  
  1     1   4  
  1     1   5  
  1     1   1  
  1     1   2  
  1     1   4  
  1     1   1  
  1     1   3  
  1     1   5  
  1     1   1  
  1     1   3  
  1     1   4  
  1     1   2  
  1     1   2  
  1     1   4  
  1     1   1  
  1     1   3  
  1     1   4  
  1     1   1  
  1     1   3  
  1     1   4  
  1     1   1  
  1     1   4  
  1     1   4  
  1     1   1  
  1     1   2  
  1     1   4  
  1     1   1  
  1     1   2  
  1     1   5  
  1     1   1  
  1     1   2  
  1     1   4  
  1     1   1  
  1     1   3  
  1     1   4  
  1     1   1  
  1     1   3  
  1     1   4  
  1     1   1  
  1     1   3  
  1         4  
  1         5  
  1         3  
  1         4  
  1         1  
  1         4  
  1         4  
  1         1  
  1         3  
  1         5  
  1         1  
  1         2  
  1         4  
  1         1  
  1         4  
  1         4  
  1         1  
  1         2  
  1         4  
  1         1  
  1         3  
  1         5  
  1         1  
  1         4  
  1         4  
  1         2  
  1         3  
  1         5  
  1         1  
  1         3  
  1         4  
  1         2  
  1         2  
  1         3  
  1         2  
  1         2  
  1         4  
  1         1  
  1         3  
  1         4  
  1         1  
  1         3  
  1         4  
  1         1  
  1         3  
  1         4  
  1         1  
  1         3  
  1         4  
  1         1  
  1         8  
  1         4  
  1         1  
  1         4  
  1         4  
  1         1  
  1         4  
  1         4  
  1         1  
  1         3  
  1         4  
  1         1  
  1         3  
  1         5  
  1         1  
  1         3  
  1         4  
  1         1  
  1         3  
  1         5  
  1         0  
  1         4  
  1         4  
  1         1  
  1         3  
  1         5  
  1         1  
  1         4  
  1         4  
  1         1  
  1         4  
  1         4  
  1         1  
  1         4  
  42         2723  
222              
223              
224 42   33     3829 my $self = bless(
225             {
226             proto => $proto,
227             class => $class,
228             },
229             ref($mp) || $mp
230             );
231 42 100       89 if (ref($proto)) {
232 21         55 my $spyClass = Class::MOP::Class->create_anon_class(superclasses => [$class]);
233 21         10612 $spyClass->rebless_instance($proto);
234 21         2554 $self->{spyClass} = $spyClass;
235             }
236              
237 42         82 $self->spyOnMethod($proto, $method);
238              
239 42         1219 return $self;
240             }
241              
242             sub stopSpying {
243 22     22   14 my $self = shift;
244 22 100       35 if(ref($self->{proto})){
245 1         3 $self->{class}->meta->rebless_instance_back($self->{proto});
246             }
247             else {
248 21         17 foreach my $method (keys %{$self->{original_methods}}){
  21         53  
249 22         92 $self->{class}->meta->remove_method($method);
250 22         840 $self->{class}->meta->add_method($method, $self->{original_methods}{$method});
251             }
252             }
253             }
254              
255             sub spyOnMethod {
256 44     44   59 my($self, $proto, $method) = @_;
257              
258 44   66     99 my $class = ref($proto) || $proto;
259 44         86 my $metaclass = $proto->meta;
260 44 50       466 $metaclass->make_mutable if ($metaclass->is_immutable);
261              
262 44         123 $self->{current_method} = $method;
263 44         76 $self->{original_methods}{$method} = $metaclass->get_method($method);
264 44         784 $metaclass->remove_method($method);
265 44         911 $self->{spyClass} = $metaclass;
266 44         62 $self->{responses}{$method} = undef;
267 44     53   168 $metaclass->add_method($method, sub { $self->__callFake($method, @_); });
  53     53   133  
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
        53      
268             }
269              
270             sub setCurrentMethod {
271 83     83   55 my $self = shift;
272 83         81 $self->{current_method} = shift;
273             }
274              
275             sub __callFake {
276 53     53   35 my $self = shift;
277 53         48 my $method = shift;
278 53 100 33     107 if($_[0] eq $self->{proto}){
    50 33        
279 52         37 shift;
280             }
281             elsif(ref($_[0]) && !ref($self->{proto}) && $_[0]->isa($self->{class})){
282 1         2 shift;
283             }
284 53         55 $self->setCurrentMethod($method);
285 53         34 push @{ $self->calls }, [@_];
  53         65  
286 53 100       131 if(ref($self->{responses}{$method}) eq 'CODE'){
    100          
287 8         13 return $self->{responses}{$method}->(@_);
288             }
289             elsif (ref($self->{responses}{$method}) eq 'Class::MOP::Method') {
290 2         7 return $self->{responses}{$method}->execute(@_);
291             }
292 43         55 return $self->__returnFromValue($self->{responses}{$method});
293             }
294              
295             sub __returnFromValue {
296 49     49   35 my $self = shift;
297 49         26 my $value = shift;
298 49 100       64 if(wantarray){
299 4 100       18 if(ref($value) eq 'ARRAY'){
    50          
300 2         7 return @$value;
301             }
302             elsif(ref($value) eq 'HASH'){
303 2         9 return %$value;
304             }
305             }
306 45         77 return $value;
307             }
308              
309             sub andReturn {
310 10     10   12 my $self = shift;
311 10         11 my($ret) = @_;
312 10         19 $self->{responses}{ $self->{current_method} } = $ret;
313             }
314              
315             sub andReturnValues {
316 2     2   3 my $self = shift;
317 2         4 my(@returns) = @_;
318 2     6   9 $self->{responses}{ $self->{current_method} } = sub { return $self->__returnFromValue(shift( @returns )) };
  6         8  
319             }
320              
321             sub calls {
322 89     89   69 my $self = shift;
323              
324 89 100       161 if(!exists $self->{calls}{ $self->{current_method} }){
325 39         36 my(@calls) = ();
326 39         105 $self->{calls}{ $self->{current_method} } = tie @calls, 'Jasmine::Spy::Instance::Calls';
327             }
328              
329 89         302 return $self->{calls}{ $self->{current_method} };
330             }
331              
332             sub andCallThrough {
333 2     2   2 my $self = shift;
334 2         3 my $toCall;
335 2 100       5 if(ref($self->{proto})){
336 1         3 $toCall = $self->{class}->meta->get_method($self->{current_method});
337             }
338             else {
339 1         2 $toCall = $self->{original_methods}{ $self->{current_method} };
340             }
341              
342 2         99 $self->andReturn($toCall);
343             }
344              
345             sub andCallFake {
346 2     2   5 shift->andReturn(@_);
347             }
348              
349             sub toHaveBeenCalled {
350 14     14   13 my($self) = shift;
351              
352 14         33 my $tb = __PACKAGE__->builder;
353              
354 14 100       107 if(want('VOID')){
355 6         261 $tb->ok(scalar(@{ $self->calls }));
  6         8  
356             }
357 14         2011 return $self->calls;
358             }
359              
360             sub notToHaveBeenCalled {
361 4     4   5 my($self) = shift;
362              
363 4         10 my $tb = __PACKAGE__->builder;
364              
365 4 50       29 if (scalar(@{ $self->calls })){
  4         5  
366 0         0 $tb->ok(0);
367 0         0 return 0;
368             }
369 4         10 $tb->ok(1);
370 4         1238 return 1;
371             }
372              
373             sub toHaveBeenCalledWith {
374 2     2   3 my($self) = shift;
375              
376 2         5 my $tb = __PACKAGE__->builder;
377              
378 2         16 my $calls = $self->__callsMatching(@_);
379              
380 2 50       7 if(want('VOID')){
381 2         89 $tb->ok(scalar(@$calls));
382             }
383              
384 2         893 return $calls;
385             }
386              
387             sub notToHaveBeenCalledWith {
388 2     2   2 my($self) = shift;
389              
390 2         6 my $tb = __PACKAGE__->builder;
391              
392              
393 2 50       14 if (scalar(@{ $self->__callsMatching(@_) })){
  2         3  
394 0         0 $tb->ok(0);
395 0         0 return 0;
396             }
397 2         6 $tb->ok(1);
398 2         574 return 1;
399             }
400              
401             sub __callsMatching {
402 4     4   4 my $self = shift;
403 4         5 my(@calls) = ();
404 4         8 my $calls = tie @calls, 'Jasmine::Spy::Instance::Calls';
405 4         9 push @$calls, grep({eq_deeply([@_], $_)} @{ $self->calls } );
  4         11  
  4         5  
406 4         8033 return $calls;
407             }
408              
409             package Jasmine::Spy::Instance::Calls;
410             $Jasmine::Spy::Instance::Calls::VERSION = '1.02';
411 1     1   419 use Tie::Array;
  1         754  
  1         19  
412 1     1   5 use Test::Builder::Module;
  1         1  
  1         6  
413 1     1   19 use vars qw(@ISA);
  1         1  
  1         199  
414             @ISA = qw(Tie::StdArray Test::Builder::Module);
415              
416             sub first {
417 0     0   0 my $self = shift;
418 0         0 return $self->[0];
419             }
420              
421             sub mostRecent {
422 4     4   4 my $self = shift;
423 4         14 return $self->[$#$self];
424             }
425              
426             sub reset {
427 2     2   2 my $self = shift;
428 2         5 @$self = ();
429             }
430              
431             sub once {
432 2     2   3 my $self = shift;
433 2         7 my $tb = __PACKAGE__->builder;
434 2         13 $tb->ok(scalar(@$self) == 1);
435             }
436             sub atleast {
437 2     2   3 my $self = shift;
438 2         2 my $times = shift;
439 2         4 my $tb = __PACKAGE__->builder;
440 2         13 $tb->ok(scalar(@$self) >= $times);
441             }
442              
443             sub atMost {
444 2     2   2 my $self = shift;
445 2         3 my $times = shift;
446 2         3 my $tb = __PACKAGE__->builder;
447 2         17 $tb->ok(scalar(@$self) <= $times);
448             }
449              
450             sub exactly {
451 2     2   3 my $self = shift;
452 2         2 my $times = shift;
453 2         5 my $tb = __PACKAGE__->builder;
454 2         36 $tb->ok(scalar(@$self) == $times);
455             }
456              
457             return 42;