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