File Coverage

blib/lib/Mock/MonkeyPatch.pm
Criterion Covered Total %
statement 69 69 100.0
branch 20 22 90.9
condition 3 5 60.0
subroutine 19 19 100.0
pod 7 7 100.0
total 118 122 96.7


line stmt bran cond sub pod time code
1             package Mock::MonkeyPatch;
2              
3 1     1   56640 use strict;
  1         2  
  1         24  
4 1     1   4 use warnings;
  1         2  
  1         35  
5              
6             our $VERSION = '1.02';
7             $VERSION = eval $VERSION;
8              
9 1     1   5 use Carp ();
  1         1  
  1         12  
10 1     1   4 use Scalar::Util ();
  1         1  
  1         15  
11 1     1   396 use Sub::Util 1.40 ();
  1         274  
  1         29  
12              
13             sub ORIGINAL;
14              
15             {
16 1     1   5 no strict 'refs';
  1         2  
  1         43  
17              
18 10     10   36 sub _defined { defined &{$_[0]} }
  10         216  
19             sub _patch {
20 1     1   4 no warnings 'redefine';
  1         2  
  1         324  
21 18     18   21 my $p = prototype \&{$_[0]};
  18         41  
22 18 100       36 if (defined $p) {
23 2         10 Sub::Util::set_prototype($p, $_[1]);
24             }
25 18         18 *{$_[0]} = $_[1];
  18         68  
26             }
27             }
28              
29             sub arguments {
30 11     11 1 517 my ($self, $occurance) = @_;
31 11 100       23 $occurance = 0 unless defined $occurance;
32 11         48 return $self->{arguments}[$occurance];
33             }
34              
35 7     7 1 1730 sub called { scalar @{$_[0]{arguments}} }
  7         36  
36              
37             sub method_arguments {
38 3     3 1 5 my ($self, $occurance, $type) = @_;
39             return undef
40 3 50       8 unless my $args = $self->arguments($occurance);
41 3         8 my @args = @$args; # copy
42 3         6 my $inst = shift @args;
43 3 100       7 if ($type) {
44             return undef
45 2 100       12 unless $inst->isa($type);
46             }
47 2         9 return \@args;
48             }
49              
50             sub patch {
51 10     10 1 13820 my ($class, $symbol, $sub, $opts) = @_;
52 10   100     43 $opts ||= {};
53              
54 10         20 $symbol =~ s/^&//;
55              
56 10 100       16 Carp::croak "Symbol &$symbol is not already defined"
57             unless _defined $symbol;
58              
59             my $self = bless {
60             arguments => [],
61 9         39 original => \&{$symbol},
62 9 100       15 store => exists $opts->{store_arguments} ? $opts->{store_arguments} : 1,
63             sub => $sub,
64             symbol => $symbol,
65             }, $class;
66              
67 9         24 Scalar::Util::weaken(my $weak = $self);
68             _patch $symbol => sub {
69 1     1   7 no warnings 'redefine';
  1         2  
  1         265  
70 10     10   770 local *ORIGINAL = $weak->{original};
71 10 100       14 push @{ $weak->{arguments} }, [ $weak->{store} ? @_ : () ];
  10         48  
72 10         24 $sub->(@_);
73 9         31 };
74              
75 9         21 return $self;
76             }
77              
78 1     1 1 3 sub reset { $_[0]{arguments} = []; $_[0] }
  1         3  
79              
80             sub restore {
81 12     12 1 950 my $self = shift;
82 12 100       29 if (my $orig = delete $self->{original}) {
83 9         13 _patch $self->{symbol}, $orig;
84             }
85 12         60 return $self;
86             }
87              
88 4 100   4 1 636 sub store_arguments { @_ == 1 ? $_[0]{store} : do { $_[0]{store} = $_[1]; $_[0] } }
  2         4  
  2         4  
89              
90             sub DESTROY {
91 9     9   2972 my $self = shift;
92 9 50 33     42 return if defined ${^GLOBAL_PHASE} && ${^GLOBAL_PHASE} eq 'DESTRUCT';
93 9         16 $self->restore;
94             }
95              
96             1;
97              
98             =head1 NAME
99              
100             Mock::MonkeyPatch - Monkey patching with test mocking in mind
101              
102             =head1 SYNOPSIS
103              
104             {
105             package MyApp;
106              
107             sub gen_item_id {
108             my $type = shift;
109             # calls external service and gets id for $type
110             }
111              
112             sub build_item {
113             my $type = shift;
114             my $item = Item->new(type => $type);
115             $item->id(gen_item_id($type));
116             return $item;
117             }
118             }
119              
120             use Test::More;
121             use MyApp;
122             use Mock::MonkeyPatch;
123              
124             my $mock = Mock::MonkeyPatch->patch(
125             'MyApp::gen_item_id' => sub { 'abcd' }
126             );
127              
128             my $item = MyApp::build_item('rubber_chicken');
129             is $item->id, 'abcd', 'building item calls MyApp::gen_random_id';
130             ok $mock->called, 'the mock was indeed called';
131             is_deeply $mock->arguments, ['rubber_chicken'], 'the mock was called with expected arguments';
132              
133             =head1 DESCRIPTION
134              
135             Mocking is a common tool, especially for testing.
136             By strategically replacing a subroutine, one can isolate segments (units) of code to test individually.
137             When this is done it is important to know that the mocked sub was actually called and with what arguments it was called.
138              
139             L injects a subroutine in the place of an existing one.
140             It returns an object by which you can revisit the manner in which the mocked subroutine was called.
141             Further when the object goes out of scope (or when the L method is called) the original subroutine is replaced.
142              
143             =head1 CONSTRUCTOR
144              
145             =head2 patch
146              
147             my $mock = Mock::MonkeyPatch->patch('MyPackage::foo' => sub { ... });
148             my $mock = Mock::MonkeyPatch->patch('MyPackage::foo' => sub { ... }, \%options);
149              
150             Mock a subroutine and return a object to represent it.
151             Takes a fully qualifed subroutine name, a subroutine reference to call in its place, and optionally a hash reference of additional constructor arguments.
152              
153             The replacement subroutine will be wrapped in a one that will store calling data, then injected in place of the original.
154             Within the replacement subroutine the original is available as the fully qualified subroutine C.
155             This can be used to inject behavior before, after, or even around the original.
156             This includes munging the arguments passed to the origial (though the actual arguments are what are stored).
157             For example usage, see L.
158              
159             Currently the optional hashref only accepts one option, an initial value for L which is true if not given.
160              
161             The wrapper will have the same prototype as the mocked function if one exists.
162             The replacement need not have any prototype, the arguments received by the wrapper will be passed to the given sub as they were received.
163             (If this doesn't make any sense to you, don't worry about it.)
164              
165             =head1 METHODS
166              
167             =head2 arguments
168              
169             my $args = $mock->arguments;
170             my $args_second_time = $mock->arguments(1);
171              
172             Returns an array reference containing the arguments that were passed to the mocked subroutine (but see also L).
173             Optionally an integer may be passed which designates the call number to fetch arguments in the same manner of indexing an array (zero indexed).
174             If not given, C<0> is assumed, representing the first time the mock was called.
175             Returns C if the mocked subroutine was not called (or was not called enough times).
176              
177             use Test::More;
178             is_deeply $mock->arguments, [1, 2, 3], 'called with the right arguments';
179              
180             =head2 called
181              
182             my $time_called = $mock->called;
183              
184             Returns the number of times the mocked subroutine was called.
185             This means that that there should be values available from L up to the value of C<< $mock->called - 1 >>.
186              
187             use Test::More;
188             ok $mock->called, 'mock was called';
189             is $mock->called, 3, 'mock was called three times';
190              
191             =head2 method_arguments
192              
193             my $args = $mock->method_arguments;
194             my $args_third_time = $mock->method_arguments(2, 'MyClass');
195              
196             A wrapper around L convenient for when the mocked subroutine is called as a method.
197             Like L it returns a subroutine reference, though it removes the first arguments which is the invocant.
198             It also can take a call number designation.
199              
200             Additionally it takes a class name to test against the invocant as C<< $invocant->isa('Class::Name') >>.
201             If the invocant is not an instance of the class or a subclass thereof it returns C.
202              
203             use Test::More;
204             is_deeply $mock->method_arguments(0, 'FrobberCo::Employee'),
205             ['some', 'arguments'], 'mock method called with known arguments on a FrobberCo::Employee instance';
206              
207             =head2 reset
208              
209             $mock = $mock->reset;
210              
211             Reset the historical information stored in the mock, including L and L.
212             Returns the mock instance for chaining if desired.
213              
214             Note that this does not restore the original method. for that, see L.
215              
216             use Test::More;
217             is $mock->called, 3, 'called 3 times';
218             is $mock->reset->called, 0, 'called zero times after reset';
219              
220             =head2 restore
221              
222             $mock = $mock->restore;
223              
224             Restore the original method to its original place in the symbol table.
225             This method is also called automatically when the object goes out of scope and is garbage collected.
226             Returns the mock instance for chaining if desired.
227             This method can only be called once!
228              
229             Note that this does not reset historical information stored in the mock, for that, see L.
230              
231             =head2 store_arguments
232              
233             $mock = $mock->store_arguments(0);
234              
235             When true, the default if not passed to the constructor, arguments passed to the mocked subroutine are stored and accessible later via L and L.
236             However sometimes this isn't desirable, especially in cases where the reference count of items in the arguments matter; notably when an object should be destroyed and the destructor's behavior is important.
237             When this is true set C to a false value and only an empty array reference will be stored.
238              
239             When used as a setter, it returns the mock instance for chaining if desired.
240              
241             =head1 COOKBOOK
242              
243             =head2 Run code before the original
244              
245             The original version of the mocked function (read: the code that was available via the symbol B)
246             is available via the fully qualified symbol C.
247             You can call this in your mock if for example you want to do some setup before calling the function.
248              
249             my $mock = $self->patch($symbol, sub {
250             # do some stuff before the original
251             do_mocked_stuff(@_);
252             # then call the original function/method
253             Mock::MonkeyPatch::ORIGINAL(@_);
254             });
255              
256             =head2 Using ORIGINAL in a nonblocking environment
257              
258             Since the C symbol is implemented via C if you want to call it after leaving the scope you need to store a reference to the function in a lexical.
259              
260             my $mock = $self->patch($symbol, sub {
261             my @args = @_;
262             my $orig = \&Mock::MonkeyPatch::ORIGINAL;
263             Mojo::IOLoop->timer(1 => sub { $orig->(@args) });
264             });
265              
266             =head1 SEE ALSO
267              
268             =over
269              
270             =item *
271              
272             L
273              
274             =item *
275              
276             L
277              
278             =item *
279              
280             L
281              
282             =back
283              
284             =head1 SOURCE REPOSITORY
285              
286             L
287              
288             =head1 AUTHOR
289              
290             Joel Berger, Ejoel.a.berger@gmail.comE
291              
292             =head1 CONTRIBUTORS
293              
294             =over
295              
296             =item *
297              
298             Doug Bell (preaction)
299              
300             =item *
301              
302             Brian Medley (bpmedley)
303              
304             =back
305              
306             =head1 COPYRIGHT AND LICENSE
307              
308             Copyright (C) 2016 by Joel Berger and L
309              
310             This library is free software; you can redistribute it and/or modify
311             it under the same terms as Perl itself.