File Coverage

blib/lib/Test/Mockingbird.pm
Criterion Covered Total %
statement 132 132 100.0
branch 16 22 72.7
condition 19 36 52.7
subroutine 26 27 96.3
pod 6 6 100.0
total 199 223 89.2


line stmt bran cond sub pod time code
1             package Test::Mockingbird;
2              
3 8     8   1648149 use strict;
  8         13  
  8         298  
4 8     8   36 use warnings;
  8         11  
  8         452  
5              
6             # TODO: Look into Sub::Install
7              
8 8     8   38 use Carp qw(croak);
  8         11  
  8         461  
9 8     8   52 use Exporter 'import';
  8         20  
  8         2122  
10              
11             our @EXPORT = qw(
12             mock
13             unmock
14             mock_scoped
15             spy
16             inject
17             restore_all
18             );
19              
20             # Store mocked data
21             my %mocked; # becomes: method => [ stack of backups ]
22              
23             =head1 NAME
24              
25             Test::Mockingbird - Advanced mocking library for Perl with support for dependency injection and spies
26              
27             =head1 VERSION
28              
29             Version 0.03
30              
31             =cut
32              
33             our $VERSION = '0.03';
34              
35             =head1 SYNOPSIS
36              
37             use Test::Mockingbird;
38              
39             # Mocking
40             Test::Mockingbird::mock('My::Module', 'method', sub { return 'mocked!' });
41              
42             # Spying
43             my $spy = Test::Mockingbird::spy('My::Module', 'method');
44             My::Module::method('arg1', 'arg2');
45             my @calls = $spy->(); # Get captured calls
46              
47             # Dependency Injection
48             Test::Mockingbird::inject('My::Module', 'Dependency', $mock_object);
49              
50             # Unmocking
51             Test::Mockingbird::unmock('My::Module', 'method');
52              
53             # Restore everything
54             Test::Mockingbird::restore_all();
55              
56             =head1 DESCRIPTION
57              
58             Test::Mockingbird provides powerful mocking, spying, and dependency injection capabilities to streamline testing in Perl.
59              
60             =head1 METHODS
61              
62             =head2 mock($package, $method, $replacement)
63              
64             Mocks a method in the specified package.
65             Supports two forms:
66              
67             mock('My::Module', 'method', sub { ... });
68              
69             or the shorthand:
70              
71             mock 'My::Module::method' => sub { ... };
72              
73             =cut
74              
75             sub mock {
76 10     10 1 841201 my ($arg1, $arg2, $arg3) = @_;
77              
78 10         69 my ($package, $method, $replacement);
79              
80             # ------------------------------------------------------------
81             # New syntax:
82             # mock 'My::Module::method' => sub { ... }
83             # ------------------------------------------------------------
84 10 100 66     163 if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
      66        
85 6         17 $package = $1;
86 6         26 $method = $2;
87 6         11 $replacement = $arg2;
88             }
89             # ------------------------------------------------------------
90             # Original syntax:
91             # mock('My::Module', 'method', sub { ... })
92             # ------------------------------------------------------------
93             else {
94 4         16 ($package, $method, $replacement) = ($arg1, $arg2, $arg3);
95             }
96              
97 10 50 33     52 croak 'Package and method are required for mocking' unless $package && $method;
98              
99 8     8   53 no strict 'refs'; # Allow symbolic references
  8         19  
  8         873  
100 10         58 my $full_method = "${package}::$method";
101              
102             # Backup original if not already mocked
103 10         19 push @{ $mocked{$full_method} }, \&{$full_method};
  10         32  
  10         57  
104              
105             # Replace with mocked version
106 8     8   50 no warnings 'redefine';
  8         13  
  8         1534  
107 10   33 0   39 *{$full_method} = $replacement || sub {};
  10         56  
108             }
109              
110             =head2 unmock($package, $method)
111              
112             Restores the original method for a mocked method.
113             Supports two forms:
114              
115             unmock('My::Module', 'method');
116              
117             or the shorthand:
118              
119             unmock 'My::Module::method';
120              
121             =cut
122              
123             sub unmock {
124 7     7 1 5760 my ($arg1, $arg2) = @_;
125              
126 7         26 my ($package, $method) = _parse_target(@_);
127              
128 7 50 33     39 croak 'Package and method are required for unmocking' unless $package && $method;
129              
130 8     8   56 no strict 'refs';
  8         39  
  8         855  
131 7         20 my $full_method = "${package}::$method";
132              
133             # Restore original method if backed up
134 7 50 33     48 if (exists $mocked{$full_method} && @{ $mocked{$full_method} }) {
  7         30  
135 7         14 my $prev = pop @{ $mocked{$full_method} };
  7         20  
136 8     8   82 no warnings 'redefine';
  8         35  
  8         2418  
137 7         16 *{$full_method} = $prev;
  7         67  
138              
139 7 100       15 delete $mocked{$full_method} unless @{ $mocked{$full_method} };
  7         53  
140             }
141             }
142              
143             =head2 mock_scoped
144              
145             Creates a scoped mock that is automatically restored when it goes out of scope.
146              
147             This behaves like C, but instead of requiring an explicit call to
148             C or C, the mock is reverted automatically when the
149             returned guard object is destroyed.
150              
151             This is useful when you want a mock to apply only within a lexical block:
152              
153             {
154             my $g = mock_scoped 'My::Module::method' => sub { 'mocked' };
155             My::Module::method(); # returns 'mocked'
156             }
157              
158             My::Module::method(); # original behaviour restored
159              
160             Supports both the longhand and shorthand forms:
161              
162             my $g = mock_scoped('My::Module', 'method', sub { ... });
163              
164             my $g = mock_scoped 'My::Module::method' => sub { ... };
165              
166             Returns a guard object whose destruction triggers automatic unmocking.
167              
168             =cut
169              
170             sub mock_scoped {
171 3     3 1 204648 my ($arg1, $arg2, $arg3) = @_;
172              
173             # Reuse mock() to install the mock
174 3         39 mock($arg1, $arg2, $arg3);
175              
176             # Determine full method name using same parsing rules
177              
178 3         11 my ($package, $method) = _parse_target(@_);
179              
180 3         7 my $full_method = "${package}::$method";
181              
182 3         15 return Test::Mockingbird::Guard->new($full_method);
183             }
184              
185             =head2 spy($package, $method)
186              
187             Wraps a method so that all calls and arguments are recorded.
188             Supports two forms:
189              
190             spy('My::Module', 'method');
191              
192             or the shorthand:
193              
194             spy 'My::Module::method';
195              
196             Returns a coderef which, when invoked, returns the list of captured calls.
197             The original method is preserved and still executed.
198              
199             =cut
200              
201             sub spy {
202 2     2 1 274211 my ($arg1, $arg2) = @_;
203              
204 2         12 my ($package, $method) = _parse_target(@_);
205              
206 2 50 33     17 croak 'Package and method are required for spying' unless $package && $method;
207              
208 8     8   54 no strict 'refs';
  8         23  
  8         13915  
209 2         9 my $full_method = "${package}::$method";
210              
211             # Backup previous layer
212 2         5 push @{ $mocked{$full_method} }, \&{$full_method};
  2         9  
  2         13  
213              
214             # Data
215 2         8 my @calls;
216              
217 8     8   75 no warnings 'redefine';
  8         15  
  8         2885  
218 2         12 *{$full_method} = sub {
219 3     3   20 push @calls, [ $full_method, @_ ];
220              
221             # Call previous layer
222 3         7 my $prev = $mocked{$full_method}[-1];
223 3         11 return $prev->(@_);
224 2         14 };
225              
226 2     2   13 return sub { @calls };
  2         858  
227             }
228              
229             =head2 inject($package, $dependency, $mock_object)
230              
231             Injects a mock dependency. Supports two forms:
232              
233             inject('My::Module', 'Dependency', $mock_object);
234              
235             or the shorthand:
236              
237             inject 'My::Module::Dependency' => $mock_object;
238              
239             The injected dependency can be restored with C or C.
240              
241             =cut
242              
243             sub inject {
244 2     2 1 206182 my ($arg1, $arg2, $arg3) = @_;
245              
246 2         4 my ($package, $dependency, $mock_object);
247              
248             # ------------------------------------------------------------
249             # New shorthand syntax:
250             # inject 'My::Module::Dependency' => $mock_obj
251             # ------------------------------------------------------------
252 2 100 66     25 if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
      66        
253 1         4 $package = $1;
254 1         3 $dependency = $2;
255 1         2 $mock_object = $arg2;
256             }
257             # ------------------------------------------------------------
258             # Original syntax:
259             # inject('My::Module', 'Dependency', $mock_obj)
260             # ------------------------------------------------------------
261             else {
262 1         21 ($package, $dependency, $mock_object) = ($arg1, $arg2, $arg3);
263             }
264              
265 2 50 33     11 croak 'Package and dependency are required for injection' unless $package && $dependency;
266              
267 8     8   87 no strict 'refs';
  8         18  
  8         683  
268 2         6 my $full_dependency = "${package}::$dependency";
269              
270             # Backup original if not already mocked
271 2         3 push @{ $mocked{$full_dependency} }, \&{$full_dependency};
  2         7  
  2         12  
272              
273 8     8   45 no warnings 'redefine';
  8         13  
  8         1173  
274              
275             # Replace with the mock object
276 2     2   9 *{$full_dependency} = sub { $mock_object };
  2         10  
  2         10  
277             }
278              
279             =head2 restore_all()
280              
281             Restores mocked methods and injected dependencies.
282              
283             Called with no arguments, it restores everything:
284              
285             restore_all();
286              
287             You may also restore only a specific package:
288              
289             restore_all 'My::Module';
290              
291             This restores all mocked methods whose fully qualified names begin with
292             C.
293              
294             =cut
295              
296             sub restore_all {
297 6     6 1 7977 my $arg = $_[0];
298              
299 8     8   45 no strict 'refs';
  8         19  
  8         324  
300 8     8   35 no warnings 'redefine';
  8         11  
  8         3907  
301              
302 6 100       26 if (defined $arg) {
303 1         3 my $package = $arg;
304              
305 1         4 for my $full_method (keys %mocked) {
306 1 50       24 next unless $full_method =~ /^\Q$package\E::/;
307              
308 1         2 while (@{ $mocked{$full_method} }) {
  2         9  
309 1         2 my $prev = pop @{ $mocked{$full_method} };
  1         3  
310 1         2 *{$full_method} = $prev;
  1         7  
311             }
312              
313 1         2 delete $mocked{$full_method};
314             }
315              
316 1         4 return;
317             }
318              
319             # Global restore
320 5         21 for my $full_method (keys %mocked) {
321 6         12 while (@{ $mocked{$full_method} }) {
  12         44  
322 6         12 my $prev = pop @{ $mocked{$full_method} };
  6         15  
323 6         11 *{$full_method} = $prev;
  6         78  
324             }
325             }
326              
327 5         22 %mocked = ();
328             }
329              
330             sub _parse_target {
331 12     12   35 my ($arg1, $arg2, $arg3) = @_;
332              
333             # Shorthand: 'Pkg::method'
334 12 100 66     172 if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
      100        
335 9         55 return ($1, $2);
336             }
337              
338             # Longhand: ('Pkg','method')
339 3         12 return ($arg1, $arg2);
340             }
341              
342             =head1 SUPPORT
343              
344             This module is provided as-is without any warranty.
345              
346             Please report any bugs or feature requests to C,
347             or through the web interface at
348             L.
349             I will be notified, and then you'll
350             automatically be notified of progress on your bug as I make changes.
351              
352             You can find documentation for this module with the perldoc command.
353              
354             perldoc Test::Mockingbird
355              
356             =cut
357              
358             1;
359              
360             package Test::Mockingbird::Guard;
361              
362             sub new {
363 3     3   8 my ($class, $full_method) = @_;
364 3         26 return bless { full_method => $full_method }, $class;
365             }
366              
367             sub DESTROY {
368 3     3   3626 my $self = $_[0];
369              
370 3         20 Test::Mockingbird::unmock($self->{full_method});
371             }
372              
373             1;