File Coverage

blib/lib/Jasmine/Spy.pm
Criterion Covered Total %
statement 290 299 96.9
branch 30 38 78.9
condition 7 15 46.6
subroutine 101 103 98.0
pod 4 4 100.0
total 432 459 94.1


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.01';
4             =head1 NAME
5              
6             Jasmine::Spy
7              
8             =head1 VERSION
9              
10             version 1.01
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   75281 use strict;
  1         2  
  1         28  
159 1     1   4 use warnings;
  1         2  
  1         33  
160 1     1   4 use vars qw(@EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         2  
  1         73  
161 1     1   5 use base qw(Exporter);
  1         1  
  1         75  
162 1     1   775 use Class::MOP;
  1         83705  
  1         80  
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         211 %EXPORT_TAGS = (
176             all => \@EXPORT_OK,
177             );
178             }
179              
180             sub spyOn {
181 40     40 1 23942 my ($proto, $method) = @_;
182 40 50       113 if(exists($spies{$proto})){
183 0         0 $spies{$proto}->spyOnMethod($proto, $method);
184             }
185             else {
186 40         81 my $spy = Jasmine::Spy::Instance->new($proto, $method);
187 40         138 $spies{$proto} = $spy;
188             }
189 40         92 return $spies{$proto};
190             }
191              
192             sub stopSpying {
193 22     22 1 6188 my ($proto) = @_;
194 22         29 my $spy = delete $spies{$proto};
195 22 100       43 if($spy){
196 21         32 $spy->stopSpying;
197             }
198             }
199              
200             sub expectSpy {
201 22     22 1 48 my($proto, $method) = @_;
202 22         36 $spies{$proto}->setCurrentMethod($method);
203 22         54 return $spies{$proto};
204             }
205              
206             sub getCalls {
207 4     4 1 15 expectSpy(@_)->calls;
208             }
209              
210             package Jasmine::Spy::Instance;
211             $Jasmine::Spy::Instance::VERSION = '1.01';
212 1     1   6 use warnings;
  1         1  
  1         24  
213 1     1   3 use strict;
  1         1  
  1         20  
214 1     1   3 use base qw(Test::Builder::Module);
  1         1  
  1         70  
215 1     1   13 use Test::Deep;
  1         1  
  1         7  
216 1     1   675 use Want;
  1         1207  
  1         850  
217              
218             sub new {
219 40     40   41 my ($mp, $proto, $method) = @_;
220 40   66     106 my $class = ref($proto) || $proto;
221 1 50   1   437 eval "package $class; use metaclass;" unless ($proto->can("metaclass"));
  1     1   322  
  1     1   5  
  1     1   4  
  1     1   1  
  1     1   4  
  1     1   4  
  1     1   1  
  1     1   3  
  1     1   4  
  1     1   1  
  1     1   3  
  1     1   4  
  1     1   2  
  1     1   3  
  1     1   4  
  1     1   1  
  1     1   3  
  1     1   4  
  1     1   2  
  1     1   3  
  1     1   4  
  1     1   1  
  1     1   6  
  1     1   5  
  1     1   1  
  1     1   3  
  1     1   4  
  1     1   2  
  1     1   2  
  1     1   5  
  1     1   1  
  1     1   3  
  1     1   5  
  1     1   1  
  1     1   6  
  1     1   5  
  1     1   1  
  1     1   3  
  1     1   5  
  1         1  
  1         3  
  1         28  
  1         1  
  1         3  
  1         4  
  1         6  
  1         4  
  1         4  
  1         1  
  1         6  
  1         4  
  1         1  
  1         3  
  1         4  
  1         2  
  1         3  
  1         5  
  1         2  
  1         2  
  1         4  
  1         2  
  1         3  
  1         5  
  1         1  
  1         4  
  1         5  
  1         1  
  1         3  
  1         5  
  1         1  
  1         3  
  1         4  
  1         2  
  1         3  
  1         5  
  1         1  
  1         3  
  1         4  
  1         1  
  1         3  
  1         4  
  1         2  
  1         3  
  1         5  
  1         1  
  1         3  
  1         4  
  1         2  
  1         3  
  1         4  
  1         1  
  1         4  
  1         4  
  1         1  
  1         3  
  1         4  
  1         2  
  1         3  
  1         4  
  1         1  
  1         3  
  1         5  
  1         1  
  1         2  
  1         4  
  1         1  
  1         3  
  1         4  
  1         1  
  1         4  
  1         4  
  1         10  
  1         4  
  1         4  
  1         2  
  1         4  
  1         5  
  1         1  
  1         3  
  40         2464  
222              
223              
224 40   33     3621 my $self = bless(
225             {
226             proto => $proto,
227             class => $class,
228             },
229             ref($mp) || $mp
230             );
231 40 100       82 if (ref($proto)) {
232 20         52 my $spyClass = Class::MOP::Class->create_anon_class(superclasses => [$class]);
233 20         10098 $spyClass->rebless_instance($proto);
234 20         2465 $self->{spyClass} = $spyClass;
235             }
236              
237 40         76 $self->spyOnMethod($proto, $method);
238              
239 40         1113 return $self;
240             }
241              
242             sub stopSpying {
243 21     21   16 my $self = shift;
244 21 100       31 if(ref($self->{proto})){
245 1         3 $self->{class}->meta->rebless_instance_back($self->{proto});
246             }
247             else {
248 20         15 foreach my $method (keys %{$self->{original_methods}}){
  20         48  
249 20         49 $self->{class}->meta->remove_method($method);
250 20         700 $self->{class}->meta->add_method($method, $self->{original_methods}{$method});
251             }
252             }
253             }
254              
255             sub spyOnMethod {
256 40     40   44 my($self, $proto, $method) = @_;
257              
258 40   66     78 my $class = ref($proto) || $proto;
259 40         80 my $metaclass = $proto->meta;
260 40 50       415 $metaclass->make_mutable if ($metaclass->is_immutable);
261              
262 40         114 $self->{current_method} = $method;
263 40         68 $self->{original_methods}{$method} = $metaclass->get_method($method);
264 40         664 $metaclass->remove_method($method);
265 40         761 $self->{spyClass} = $metaclass;
266 40         54 $self->{responses}{$method} = undef;
267 40     49   144 $metaclass->add_method($method, sub { $self->__callFake($method, @_); });
  49     49   118  
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
        49      
268             }
269              
270             sub setCurrentMethod {
271 22     22   26 my $self = shift;
272 22         22 $self->{current_method} = shift;
273             }
274              
275             sub __callFake {
276 49     49   39 my $self = shift;
277 49         33 my $method = shift;
278 49 100 33     118 if($_[0] eq $self->{proto}){
    50 33        
279 48         34 shift;
280             }
281             elsif(ref($_[0]) && !ref($self->{proto}) && $_[0]->isa($self->{class})){
282 1         1 shift;
283             }
284              
285 49         34 push @{ $self->calls }, [@_];
  49         58  
286 49 100       118 if(ref($self->{responses}{$method}) eq 'CODE'){
    100          
287 8         14 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 39         55 return $self->__returnFromValue($self->{responses}{$method});
293             }
294              
295             sub __returnFromValue {
296 45     45   23 my $self = shift;
297 45         35 my $value = shift;
298 45 100       62 if(wantarray){
299 4 100       12 if(ref($value) eq 'ARRAY'){
    50          
300 2         17 return @$value;
301             }
302             elsif(ref($value) eq 'HASH'){
303 2         10 return %$value;
304             }
305             }
306 41         71 return $value;
307             }
308              
309             sub andReturn {
310 10     10   11 my $self = shift;
311 10         12 my($ret) = @_;
312 10         18 $self->{responses}{ $self->{current_method} } = $ret;
313             }
314              
315             sub andReturnValues {
316 2     2   4 my $self = shift;
317 2         4 my(@returns) = @_;
318 2     6   8 $self->{responses}{ $self->{current_method} } = sub { return $self->__returnFromValue(shift( @returns )) };
  6         10  
319             }
320              
321             sub calls {
322 73     73   51 my $self = shift;
323              
324 73 100       132 if(!exists $self->{calls}{ $self->{current_method} }){
325 35         35 my(@calls) = ();
326 35         120 $self->{calls}{ $self->{current_method} } = tie @calls, 'Jasmine::Spy::Instance::Calls';
327             }
328              
329 73         263 return $self->{calls}{ $self->{current_method} };
330             }
331              
332             sub andCallThrough {
333 2     2   3 my $self = shift;
334 2         2 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         14 $toCall = $self->{original_methods}{ $self->{current_method} };
340             }
341              
342 2         102 $self->andReturn($toCall);
343             }
344              
345             sub andCallFake {
346 2     2   3 shift->andReturn(@_);
347             }
348              
349             sub toHaveBeenCalled {
350 10     10   9 my($self) = shift;
351              
352 10         23 my $tb = __PACKAGE__->builder;
353              
354 10 100       77 if(want('VOID')){
355 2         126 $tb->ok(scalar(@{ $self->calls }));
  2         8  
356             }
357 10         641 return $self->calls;
358             }
359              
360             sub notToHaveBeenCalled {
361 4     4   4 my($self) = shift;
362              
363 4         11 my $tb = __PACKAGE__->builder;
364              
365 4 50       30 if (scalar(@{ $self->calls })){
  4         5  
366 0         0 $tb->ok(0);
367 0         0 return 0;
368             }
369 4         9 $tb->ok(1);
370 4         770 return 1;
371             }
372              
373             sub toHaveBeenCalledWith {
374 2     2   4 my($self) = shift;
375              
376 2         5 my $tb = __PACKAGE__->builder;
377              
378 2         19 my $calls = $self->__callsMatching(@_);
379              
380 2 50       6 if(want('VOID')){
381 2         91 $tb->ok(scalar(@$calls));
382             }
383              
384 2         396 return $calls;
385             }
386              
387             sub notToHaveBeenCalledWith {
388 2     2   3 my($self) = shift;
389              
390 2         6 my $tb = __PACKAGE__->builder;
391              
392              
393 2 50       13 if (scalar(@{ $self->__callsMatching(@_) })){
  2         5  
394 0         0 $tb->ok(0);
395 0         0 return 0;
396             }
397 2         10 $tb->ok(1);
398 2         369 return 1;
399             }
400              
401             sub __callsMatching {
402 4     4   3 my $self = shift;
403 4         5 my(@calls) = ();
404 4         7 my $calls = tie @calls, 'Jasmine::Spy::Instance::Calls';
405 4         10 push @$calls, grep({eq_deeply([@_], $_)} @{ $self->calls } );
  4         12  
  4         6  
406 4         7800 return $calls;
407             }
408              
409             package Jasmine::Spy::Instance::Calls;
410             $Jasmine::Spy::Instance::Calls::VERSION = '1.01';
411 1     1   421 use Tie::Array;
  1         766  
  1         21  
412 1     1   4 use Test::Builder::Module;
  1         1  
  1         7  
413 1     1   20 use vars qw(@ISA);
  1         1  
  1         200  
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 0     0   0 my $self = shift;
423 0         0 return $self->[$#$self];
424             }
425              
426             sub reset {
427 2     2   3 my $self = shift;
428 2         4 @$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         6 my $tb = __PACKAGE__->builder;
440 2         12 $tb->ok(scalar(@$self) >= $times);
441             }
442              
443             sub atMost {
444 2     2   3 my $self = shift;
445 2         2 my $times = shift;
446 2         5 my $tb = __PACKAGE__->builder;
447 2         13 $tb->ok(scalar(@$self) <= $times);
448             }
449              
450             sub exactly {
451 2     2   3 my $self = shift;
452 2         3 my $times = shift;
453 2         4 my $tb = __PACKAGE__->builder;
454 2         14 $tb->ok(scalar(@$self) == $times);
455             }
456              
457             return 42;