File Coverage

blib/lib/Test/Mockify.pm
Criterion Covered Total %
statement 196 200 98.0
branch 51 54 94.4
condition 7 11 63.6
subroutine 32 32 100.0
pod 3 4 75.0
total 289 301 96.0


line stmt bran cond sub pod time code
1             =pod
2              
3             =head1 NAME
4              
5             Test::Mockify - minimal mocking framework for perl
6              
7             =head1 SYNOPSIS
8              
9             use Test::Mockify;
10             use Test::Mockify::Verify qw ( WasCalled );
11             use Test::Mockify::Matcher qw ( String );
12              
13             # build a new mocked object
14             my $MockObjectBuilder = Test::Mockify->new('SampleLogger', []);
15             $MockObjectBuilder->mock('log')->when(String())->thenReturnUndef();
16             my $MockedLogger = $MockLoggerBuilder->getMockObject();
17              
18             # inject mocked object into the code you want to test
19             my $App = SampleApp->new('logger'=> $MockedLogger);
20             $App->do_something();
21              
22             # verify that the mocked method was called
23             ok(WasCalled($MockedLogger, 'log'), 'log was called');
24             done_testing();
25              
26             =head1 DESCRIPTION
27              
28             Use L to create and configure mock objects. Use L to
29             verify the interactions with your mocks. Use L to inject dependencies into your Sut.
30              
31             You can find a Example Project in L
32              
33             It is possible to use alternative constructor name
34             my $MockObjectBuilder = Test::Mockify->new('SampleLogger', [], 'create');
35              
36             =head1 METHODS
37              
38             =cut
39              
40             package Test::Mockify;
41 17     17   453758 use Test::Mockify::Tools qw ( Error ExistsMethod LoadPackage );
  17         60  
  17         1102  
42 17     17   9163 use Test::Mockify::TypeTests qw ( IsString IsArrayReference);
  17         48  
  17         1005  
43 17     17   7035 use Test::Mockify::MethodCallCounter;
  17         45  
  17         470  
44 17     17   6724 use Test::Mockify::Method;
  17         66  
  17         565  
45 17     17   7058 use Test::Mockify::MethodSpy;
  17         53  
  17         519  
46 17     17   8191 use Test::MockObject::Extends;
  17         121671  
  17         89  
47 17     17   618 use Scalar::Util qw( blessed );
  17         38  
  17         771  
48 17     17   7744 use Sub::Override;
  17         17192  
  17         550  
49              
50 17     17   110 use strict;
  17         53  
  17         35641  
51              
52             our $VERSION = '2.4';
53              
54             sub new {
55 114     114 0 38858 my $class = shift;
56 114         258 my ( $FakeModulePath, $aFakeParams, $AlternativeConstructorName ) = @_;
57              
58 114         238 my $self = bless {}, $class;
59              
60 114   100     550 $AlternativeConstructorName //= 'new';
61 114         392 LoadPackage( $FakeModulePath );
62 114 100       739 if(!$FakeModulePath->can($AlternativeConstructorName)){
63 27 100       70 if(defined $aFakeParams ){
64 1         10 Error("'$FakeModulePath' have no constructor. If you like to create a mock of a package without constructor please use it without parameter list");
65             }else{
66 26         69 $self->{'MockStaticModule'} = 1;
67             }
68             }
69 113 100       272 my $FakeClass = $aFakeParams ? $FakeModulePath->$AlternativeConstructorName( @{$aFakeParams} ) : $FakeModulePath;
  80         262  
70 113         1041 $self->_mockedModulePath($FakeModulePath);
71 113         441 $self->_mockedSelf(Test::MockObject::Extends->new( $FakeClass ));
72 113         297 $self->_initMockedModule();
73              
74 113         276 return $self;
75              
76             }
77             #----------------------------------------------------------------------------------------
78             sub _mockedModulePath {
79 273     273   355 my $self = shift;
80 273         407 my ($ModulePath) = @_;
81 273 100       924 return $self->{'MockedModulePath'} unless ($ModulePath);
82 113         274 $self->{'MockedModulePath'} = $ModulePath;
83             }
84             #----------------------------------------------------------------------------------------
85             sub _mockedSelf {
86 905     905   14903 my $self = shift;
87 905         1242 my ($MockedSelf) = @_;
88 905 100       3597 return $self->{'MockedModule'} unless ($MockedSelf);
89 113         256 $self->{'MockedModule'} = $MockedSelf;
90             }
91             #----------------------------------------------------------------------------------------
92             sub _initMockedModule {
93 113     113   147 my $self = shift;
94              
95 113         404 $self->_mockedSelf()->{'__MethodCallCounter'} = Test::Mockify::MethodCallCounter->new();
96 113         236 $self->_mockedSelf()->{'__isMockified'} = 1;
97 113         315 $self->_addGetParameterFromMockifyCall();
98              
99 113         430 $self->{'__override'} = Sub::Override->new();
100 113         879 $self->_mockedSelf()->{'__override'} = $self->{'__override'};
101 113         220 $self->{'IsStaticMockStore'} = undef;
102 113         173 $self->{'IsImportedMockStore'} = undef;
103 113         150 return;
104             }
105              
106             #----------------------------------------------------------------------------------------
107             =pod
108              
109             =head2 getMockObject
110              
111             Provides the actual mock object, which you can use in the test.
112              
113             my $aParameterList = ['SomeValueForConstructor'];
114             my $MockObjectBuilder = Test::Mockify->new( 'My::Module', $aParameterList );
115             my $MyModuleObject = $MockObjectBuilder->getMockObject();
116              
117             =cut
118             sub getMockObject {
119 98     98 1 171 my $self = shift;
120 98         220 return $self->_mockedSelf();
121             }
122              
123             #----------------------------------------------------------------------------------------=
124             =pod
125              
126             =head2 mock
127              
128             This is the place where the mocked methods are defined. The method also proves that the method you like to mock actually exists.
129              
130             =head3 synopsis
131              
132             This method takes one parameter, which is the name of the method you like to mock.
133             Because you need to specify more detailed the behaviour of this mock you have to chain the method signature (when) and the expected return value (then...).
134              
135             For example, the next line will create a mocked version of the method log, but only if this method is called with any string and the number 123. In this case it will return the String 'Hello World'. Mockify will throw an error if this method is called somehow else.
136              
137             my $MockObjectBuilder = Test::Mockify->new( 'Sample::Logger', [] );
138             $MockObjectBuilder->mock('log')->when(String(), Number(123))->thenReturn('Hello World');
139             my $SampleLogger = $MockObjectBuilder->getMockObject();
140             is($SampleLogger->log('abc',123), 'Hello World');
141              
142              
143             =head4 when
144              
145             To define the signature in the needed structure you must use the L.
146              
147             =head4 whenAny
148              
149             If you don't want to specify the method signature at all, you can use whenAny.
150             It is not possible to mix C and C for the same method.
151              
152             =head4 then ...
153              
154             For possible return types please look in L
155              
156             =cut
157             sub mock {
158 66     66 1 353 my $self = shift;
159 66         124 my @Parameters = @_;
160              
161 66         103 my $ParameterAmount = scalar @Parameters;
162 66 50 33     288 if($ParameterAmount == 1 && IsString($Parameters[0]) ){
163 66         130 $self->{'__UsedSubMock'} = 1;
164 66         154 return $self->_addMockWithMethod($Parameters[0]);
165             }else{
166 0         0 Error('"mock" Needs to be called with one Parameter which needs to be a String. ');
167             }
168 0         0 return;
169             }
170             #----------------------------------------------------------------------------------------
171              
172              
173              
174              
175             =pod
176              
177              
178             =head2 spy
179              
180             Use spy if you want to observe a method. You can use the L to ensure that the method was called with the expected parameters.
181              
182             =head3 synopsis
183              
184             This method takes one parameter, which is the name of the method you like to spy.
185             Because you need to specify more detailed the behaviour of this spy you have to define the method signature with C
186              
187             For example, the next line will create a method spy of the method log, but only if this method is called with any string and the number 123. Mockify will throw an error if this method is called in another way.
188              
189             my $MockObjectBuilder = Test::Mockify->new( 'Sample::Logger', [] );
190             $MockObjectBuilder->spy('log')->when(String(), Number(123));
191             my $SampleLogger = $MockObjectBuilder->getMockObject();
192              
193             # call spied method
194             $SampleLogger->log('abc', 123);
195              
196             # verify that the spied method was called
197             is_deeply(GetParametersFromMockifyCall($MockedLogger, 'log'),['abc', 123], 'Check parameters of first call');
198              
199             =head4 when
200              
201             To define the signature in the needed structure you must use the L.
202              
203             =head4 whenAny
204              
205             If you don't want to specify the method signature at all, you can use whenAny.
206             It is not possible to mix C and C for the same method.
207              
208             =cut
209             sub spy {
210 16     16 1 78 my $self = shift;
211 16         24 my ($MethodName) = @_;
212 16         19 my $PointerOriginalMethod = \&{sprintf ('%s::%s', $self->_mockedModulePath(), $MethodName)};
  16         27  
213             #In order to have the current object available in the parameter list, it has to be injected here.
214             return $self->_addMockWithMethodSpy($MethodName, sub {
215 15     15   31 return $PointerOriginalMethod->($self->_mockedSelf(), @_);
216 16         79 });
217             }
218             #----------------------------------------------------------------------------------------
219             sub _addMockWithMethod {
220 97     97   146 my $self = shift;
221 97         161 my ( $MethodName ) = @_;
222 97         256 $self->_testMockTypeUsage($MethodName);
223 96 100       285 if($self->{'IsStaticMockStore'}{$MethodName}){
    100          
224 18         99 return $self->_addStaticMock($MethodName, Test::Mockify::Method->new());
225             }elsif($self->{'IsImportedMockStore'}{$MethodName}){
226 13         61 return $self->_addImportedMock($MethodName, Test::Mockify::Method->new());
227             }else{
228 65         224 return $self->_addMock($MethodName, Test::Mockify::Method->new());
229             }
230             }
231             #----------------------------------------------------------------------------------------
232             sub _addMockWithMethodSpy {
233 26     26   41 my $self = shift;
234 26         47 my ( $MethodName, $PointerOriginalMethod ) = @_;
235 26         64 $self->_testMockTypeUsage($MethodName);
236 25 100       65 if($self->{'IsStaticMockStore'}{$MethodName}){
    100          
237 5         31 return $self->_addStaticMock($MethodName, Test::Mockify::MethodSpy->new($PointerOriginalMethod));
238             }elsif($self->{'IsImportedMockStore'}{$MethodName}){
239 5         29 return $self->_addImportedMock($MethodName, Test::Mockify::MethodSpy->new($PointerOriginalMethod));
240             }else{
241 15         47 return $self->_addMock($MethodName, Test::Mockify::MethodSpy->new($PointerOriginalMethod));
242             }
243             }
244             #-------------------------------------------------------------------------------------
245             sub _addMock {
246 80     80   115 my $self = shift;
247 80         148 my ($MethodName, $Method) = @_;
248              
249 80         145 ExistsMethod( $self->_mockedModulePath(), $MethodName );
250 79         160 $self->_mockedSelf()->{'__MethodCallCounter'}->addMethod( $MethodName );
251 79 100       236 if(not $self->{'MethodStore'}{$MethodName}){
252 69 100       145 if($self->{'MockStaticModule'}){
253 1         5 return $self->_addStaticMock($MethodName, Test::Mockify::Method->new());
254             }else{
255 68   33     283 $self->{'MethodStore'}{$MethodName} //= $Method;
256             $self->_mockedSelf()->mock($MethodName, sub {
257 82     82   6805 my $MockedSelf = shift;
258 82         394 $MockedSelf->{'__MethodCallCounter'}->increment( $MethodName );
259 82         146 my @MockedParameters = @_;
260 82         99 push @{$MockedSelf->{$MethodName.'_MockifyParams'}}, \@MockedParameters;
  82         255  
261 82 100       193 my $WantAList = wantarray ? 1 : 0;
262 82         178 return _callInjectedMethod($Method, \@MockedParameters, $WantAList, $MethodName);
263 68         114 });
264             }
265             }
266 78         2323 return $self->{'MethodStore'}{$MethodName};
267             }
268             #----------------------------------------------------------------------------------------
269             sub _callInjectedMethod {
270             # my $self = shift; #In Order to keep the mockify object out of the mocked method, I can't use the self.
271 138     138   271 my ($Method, $aMockedParameters, $WantAList, $MethodName) = @_;
272 138         177 my $ReturnValue;
273             my @ReturnValue;
274 138         186 eval {
275 138 100       256 if($WantAList){
276 16         20 @ReturnValue = $Method->call(@{$aMockedParameters});
  16         55  
277             }else{
278 122         151 $ReturnValue = $Method->call(@{$aMockedParameters});
  122         334  
279             }
280             };
281             # $@ -> current error
282 138 100       460 if ($@) {
283 19         109 Error("\nError when calling method '$MethodName'\n".$@)
284             }
285 119 100       224 if($WantAList){
286 16         93 return @ReturnValue;
287             }else{
288 103         473 return $ReturnValue;
289             }
290 0         0 return;
291             }
292             #----------------------------------------------------------------------------------------
293             sub _buildMockSub{
294 38     38   59 my $self = shift;
295 38         80 my ($MockedSelf, $MethodName, $Method) = @_;
296             # The Sub::Override lexical scope don't get released if there is any Mockify var is pointing to it.
297             # So the $MockedSelf needs to be resolved outside of the sub lexical scope.
298 38         66 my $MethodCallCounter = \$MockedSelf->{'__MethodCallCounter'};
299 38         110 my $MockifyParamsStore = \$MockedSelf->{$MethodName.'_MockifyParams'};
300 38         59 my $MustUseShift = $self->{'__UsedSubMock'};
301             return sub {
302 56 100   56   158 shift @_ if($MustUseShift);
303 56         108 ${$MethodCallCounter}->increment( $MethodName );
  56         249  
304 56         111 my @MockedParameters = @_;
305 56         64 push( @{${$MockifyParamsStore}}, \@MockedParameters );
  56         89  
  56         178  
306 56 100       128 my $WantAList = wantarray ? 1 : 0;
307 56         120 return _callInjectedMethod($Method, \@MockedParameters, $WantAList, $MethodName);
308 38         170 };
309             }
310             #----------------------------------------------------------------------------------------
311             sub _addStaticMock {
312 24     24   37 my $self = shift;
313 24         48 my ( $MethodName, $Method) = @_;
314              
315 24         55 ExistsMethod( $self->_mockedModulePath(), $MethodName );
316 24         50 $self->_mockedSelf()->{'__MethodCallCounter'}->addMethod( $MethodName );
317 24 100       74 if(not $self->{'MethodStore'}{$MethodName}){
318 22         44 $self->{'MethodStore'}{$MethodName} = $Method;
319 22         53 my $MockedSelf = $self->_mockedSelf();
320 22         88 my $MockedMethodBody = $self->_buildMockSub($MockedSelf, $MethodName, $Method);
321 22 100       167 if(!($MethodName =~ qr/::/sm)){
322 1         6 $self->_overrideInternalFunction($MethodName, $MockedMethodBody);
323             }else{
324 21         79 $self->_overrideExternalFunction($MethodName, $MockedMethodBody);
325             }
326             }
327 24         148 return $self->{'MethodStore'}{$MethodName};
328             }
329             #----------------------------------------------------------------------------------------
330             sub _overrideInternalFunction {
331 1     1   3 my $self = shift;
332 1         4 my ($MethodName, $MockedMethodBody) = @_;
333              
334 1         4 my $FullyQualifiedMethodName = sprintf('%s::%s', $self->_mockedModulePath(), $MethodName);
335 1         5 $self->_replaceWithPrototype($FullyQualifiedMethodName, $MockedMethodBody);
336              
337 1         2 return;
338             }
339             #----------------------------------------------------------------------------------------
340             sub _overrideExternalFunction {
341 21     21   44 my $self = shift;
342 21         37 my ($FullyQualifiedMethodName, $MockedMethodBody) = @_;
343 21         62 $self->_replaceWithPrototype($FullyQualifiedMethodName, $MockedMethodBody);
344 21         37 return;
345             }
346             #----------------------------------------------------------------------------------------
347             sub _replaceWithPrototype {
348 38     38   50 my $self = shift;
349 38         62 my ($FullyQualifiedMethodName, $Sub) = @_;
350 38 50       121 Error ('not a code ref ') unless ref $Sub eq 'CODE';
351 38         126 my $Prototype = prototype($FullyQualifiedMethodName);
352              
353 38 100       90 if(defined $Prototype){
354 33         2500 my $SubWithPrototype = eval( 'return sub ('. $Prototype .') {$Sub->(@_)}'); ## no critic (ProhibitStringyEval RequireInterpolationOfMetachars ) This is the only dynamic way to add the prototype
355 33 50       105 Error($@, {'Error in eval, line' => __LINE__ - 1}) if($@); # Rethrow error if something went wrong in the eval. (For debugging)
356 33         137 $self->{'__override'}->replace($FullyQualifiedMethodName, $SubWithPrototype);
357 33         1395 return;
358             }
359 5         19 $self->{'__override'}->replace($FullyQualifiedMethodName, $Sub);
360 5         233 return;
361             }
362             #----------------------------------------------------------------------------------------
363             sub _addImportedMock {
364 18     18   31 my $self = shift;
365 18         33 my ( $MethodName, $Method) = @_;
366              
367             ExistsMethod(
368             $self->{'IsImportedMockStore'}{$MethodName}->{'Path'},
369 18         55 $self->{'IsImportedMockStore'}{$MethodName}->{'MethodName'},
370             {'Mock Imported In' => $self->_mockedModulePath()}
371             );
372              
373 18         48 $self->_mockedSelf()->{'__MethodCallCounter'}->addMethod( $MethodName );
374 18 100       66 if(not $self->{'MethodStore'}{$MethodName}){
375 16         40 $self->{'MethodStore'}{$MethodName} = $Method;
376 16         44 my $MockedSelf = $self->_mockedSelf();
377 16         64 my $MockedMethodBody = $self->_buildMockSub($MockedSelf, $MethodName, $Method);
378 16         43 my $FullyQualifiedMethodName = sprintf ('%s::%s', $self->_mockedModulePath(), $self->{'IsImportedMockStore'}{$MethodName}->{'MethodName'});
379 16         56 $self->_replaceWithPrototype( $FullyQualifiedMethodName, $MockedMethodBody );
380             }
381 18         104 return $self->{'MethodStore'}{$MethodName};
382             }
383              
384             #----------------------------------------------------------------------------------------
385             sub _addGetParameterFromMockifyCall {
386 113     113   153 my $self = shift;
387              
388             $self->_mockedSelf()->mock('__getParametersFromMockifyCall',
389             sub{
390 20     20   1514 my $MockedSelf = shift;
391 20         40 my ( $MethodName, $Position ) = @_;
392              
393 20         81 my $aParametersFromAllCalls = $MockedSelf->{$MethodName.'_MockifyParams'};
394 20 100       74 if( ref $aParametersFromAllCalls ne 'ARRAY' ){
395 1         5 Error( "$MethodName was not called" );
396             }
397 19 100       27 if( scalar @{$aParametersFromAllCalls} < $Position ) {
  19         49  
398 1         17 Error( "$MethodName was not called ".( $Position+1 ).' times',{
399             'Method' => "$MethodName",
400             'Postion' => $Position,
401             } );
402             }
403             else {
404 18         54 my $ParameterFromMockifyCall = $MockedSelf->{$MethodName.'_MockifyParams'}[$Position];
405 18         78 return $ParameterFromMockifyCall;
406             }
407 0         0 return;
408             }
409 113         191 );
410              
411 113         3948 return;
412             }
413             #----------------------------------------------------------------------------------------
414             sub _testMockTypeUsage {
415 123     123   148 my $self = shift;
416 123         182 my ($MethodName) = @_;
417 123         177 my $PositionInCallerStack = 2;
418 123         515 my $MethodMockType = (caller($PositionInCallerStack))[3]; # autodetect mock type (spy or mock)
419 123 100 100     1055 if($self->{'MethodMockType'}{$MethodName} && $self->{'MethodMockType'}{$MethodName} ne $MethodMockType){
420 2         6 Error('It is not possible to mix spy and mock');
421             }else{
422 121         262 $self->{'MethodMockType'}{$MethodName} = $MethodMockType;
423             }
424 121         182 return;
425             }
426             1;
427              
428             __END__