File Coverage

blib/lib/Test/ExpectAndCheck.pm
Criterion Covered Total %
statement 121 134 90.3
branch 28 36 77.7
condition 4 6 66.6
subroutine 28 30 93.3
pod 3 3 100.0
total 184 209 88.0


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2021 -- leonerd@leonerd.org.uk
5              
6             package Test::ExpectAndCheck;
7              
8 3     3   61898 use strict;
  3         9  
  3         73  
9 3     3   13 use warnings;
  3         4  
  3         97  
10              
11             our $VERSION = '0.04';
12              
13 3     3   13 use Carp;
  3         5  
  3         149  
14              
15 3     3   15 use List::Util qw( first );
  3         5  
  3         251  
16 3     3   19 use Scalar::Util qw( blessed );
  3         6  
  3         122  
17              
18 3     3   1565 use Test::Deep ();
  3         24755  
  3         81  
19              
20 3     3   19 use constant EXPECTATION_CLASS => "Test::ExpectAndCheck::_Expectation";
  3         3  
  3         2243  
21              
22             =head1 NAME
23              
24             C - C-style unit testing with object methods
25              
26             =head1 SYNOPSIS
27              
28             use Test::More;
29             use Test::ExpectAndCheck;
30              
31             my ( $controller, $mock ) = Test::ExpectAndCheck->create;
32              
33             {
34             $controller->expect( act => 123, 45 )
35             ->will_return( 678 );
36              
37             is( $mock->act( 123, 45 ), 678, '$mock->act returns result' );
38              
39             $controller->check_and_clear( '->act' );
40             }
41              
42             done_testing;
43              
44             =head1 DESCRIPTION
45              
46             This package creates objects that assist in writing unit tests with mocked
47             object instances. Each mock instance will expect to receive a given list of
48             method calls. Each method call is checked that it received the right
49             arguments, and will return a prescribed result. At the end of each test, each
50             object is checked to ensure all the expected methods were called.
51              
52             =cut
53              
54             =head1 METHODS
55              
56             =cut
57              
58             =head2 create
59              
60             ( $controller, $mock ) = Test::ExpectAndCheck->create;
61              
62             Objects are created in "entangled pairs" by the C method. The first
63             object is called the "controller", and is used by the unit testing script to
64             set up what method calls are to be expected, and what their results shall be.
65             The second object is the "mock", the object to be passed to the code being
66             tested, on which the expected method calls are (hopefully) invoked. It will
67             have whatever interface is implied by the method call expectations.
68              
69             =cut
70              
71             sub create
72             {
73 2     2 1 149 my $class = shift;
74              
75 2         16 my $controller = bless {
76             expectations => [],
77             }, $class;
78 2         10 my $mock = Test::ExpectAndCheck::_Obj->new( $controller );
79              
80 2         6 return ( $controller, $mock );
81             }
82              
83             =head2 expect
84              
85             $exp = $controller->expect( $method, @args )
86              
87             Specifies that the mock will expect to receive a method call of the given
88             name, with the given arguments.
89              
90             The argument values are compared using L. Values can
91             be specified literally, or using any of the "Special Comparisons" defined by
92             L.
93              
94             The test script can call the L or L methods on the
95             expectation to set what the result of invoking this method will be.
96              
97             =cut
98              
99             sub expect
100             {
101 14     14 1 27489 my $self = shift;
102 14         29 my ( $method, @args ) = @_;
103              
104 14         29 my ( undef, $file, $line ) = caller(1);
105 14 50       81 defined $file or ( undef, $file, $line ) = caller(0);
106              
107 14         26 push @{ $self->{expectations} }, my $exp = $self->EXPECTATION_CLASS->new(
  14         67  
108             $method => [ @args ], $file, $line,
109             );
110              
111 14         57 return $exp;
112             }
113              
114             sub _stringify
115             {
116 8     8   14 my ( $v ) = @_;
117 8 50 33     109 if( !defined $v ) {
    50          
    100          
    50          
118 0         0 return "undef";
119             }
120             elsif( blessed $v and $v->isa( "Test::Deep::Ignore" ) ) {
121 0         0 return "ignore()";
122             }
123             elsif( $v =~ m/^-?[0-9]+$/ ) {
124 2         240 return sprintf "%d", $v;
125             }
126             elsif( $v =~ m/^[\x20-\x7E]*\z/ ) {
127 6         18 $v =~ s/([\\'])/\\$1/g;
128 6         48 return qq('$v');
129             }
130             else {
131 0 0       0 if( $v =~ m/[^\n\x20-\x7E]/ ) {
132             # string contains something non-printable; just hexdump it all
133 0         0 $v =~ s{(.)}{sprintf "\\x%02X", ord $1}gse;
  0         0  
134             }
135             else {
136 0         0 $v =~ s/([\\'\$\@])/\\$1/g;
137 0         0 $v =~ s{\n}{\\n}g;
138             }
139 0         0 return qq("$v");
140             }
141             }
142              
143             sub _stringify_args
144             {
145 17     17   54 join ", ", map { _stringify $_ } @_;
  8         13  
146             }
147              
148             sub _call
149             {
150 14     14   18 my $self = shift;
151 14         28 my ( $method, @args ) = @_;
152              
153 14         15 my $e;
154 13     13   27 $e = first { !$_->_called } @{ $self->{expectations} } and
  14         41  
155 14 100 100     43 $e->_consume( $method, @args ) or do {
156 2         15 my $message = Carp::shortmess( "Unexpected call to ->$method(${\ _stringify_args @args })" );
  2         5  
157 2 100       68 $message .= "... while expecting " . $e->_stringify if $e;
158 2 100       6 $message .= "... after all expectations done" if !$e;
159 2         14 die "$message.\n";
160             };
161              
162 12         54 return $e->_result;
163             }
164              
165             =head2 check_and_clear
166              
167             $controller->check_and_clear( $name );
168              
169             Checks that by now, every expected method has been called, and emits a new
170             test output line via L. Regardless, the expectations are also
171             cleared out ready for the start of the next test.
172              
173             =cut
174              
175             sub check_and_clear
176             {
177 15     15 1 4371 my $self = shift;
178 15         24 my ( $name ) = @_;
179              
180 15         39 my $builder = Test::Builder->new;
181 15         73 local $Test::Builder::Level = $Test::Builder::Level + 1;
182              
183             $builder->subtest( $name, sub {
184 15     15   8567 my $count = 0;
185 15         20 foreach my $exp ( @{ $self->{expectations} } ) {
  15         30  
186 14         37 $exp->_check( $builder );
187 14         257 $count++;
188             }
189              
190 15 100       38 $builder->ok( 1, "No calls made" ) if !$count;
191 15         79 });
192              
193 15         14918 undef @{ $self->{expectations} };
  15         72  
194             }
195              
196             package
197             Test::ExpectAndCheck::_Expectation;
198              
199 3     3   21 use List::Util qw( all );
  3         6  
  3         217  
200              
201             use constant {
202 3         2583 METHOD => 0,
203             ARGS => 1,
204             FILE => 2,
205             LINE => 3,
206             CALLED => 4,
207             RETURNS => 5,
208             THROWS => 6,
209             DIAG => 7,
210             ALSO => 8,
211 3     3   26 };
  3         5  
212              
213             =head1 EXPECTATIONS
214              
215             Each value returned by the L method is an "expectation", an object
216             that represents one expected method call, the arguments it should receive, and
217             the return value it should provide.
218              
219             =cut
220              
221             sub new
222             {
223 14     14   20 my $class = shift;
224 14         24 my ( $method, $args, $file, $line ) = @_;
225 14         42 return bless [ $method, $args, $file, $line, 0 ], $class;
226             }
227              
228             =head2 will_return
229              
230             $exp->will_return( @result );
231              
232             I
233              
234             Sets the result that will be returned by this method call.
235              
236             This method used to be named C, which should be avoided in new code.
237             Uses of the old name will print a deprecation warning.
238              
239             =cut
240              
241             sub will_return
242             {
243 10     10   102 my $self = shift;
244              
245 10         19 $self->[RETURNS] = [ @_ ];
246 10         14 undef $self->[THROWS];
247              
248 10         24 return $self;
249             }
250              
251             sub returns
252             {
253 0     0   0 warnings::warnif deprecated => "Calling \$exp->returns() is now deprecated; use ->will_return instead";
254 0         0 return shift->will_return( @_ );
255             }
256              
257             =head2 will_throw
258              
259             $exp->will_throw( $e );
260              
261             I
262              
263             Sets the exception that will be thrown by this method call.
264              
265             This method used to be named C, which should be avoided in new code.
266              
267             =cut
268              
269             sub will_throw
270             {
271 1     1   2 my $self = shift;
272 1         3 ( $self->[THROWS] ) = @_;
273              
274 1         2 return $self;
275             }
276              
277             sub throws
278             {
279 0     0   0 warnings::warnif deprecated => "Calling \$exp->throws() is now deprecated; use ->will_throw instead";
280 0         0 return shift->will_throw( @_ );
281             }
282              
283             =head2 will_also
284              
285             $exp->will_also( sub { ... } );
286              
287             I
288              
289             Adds extra code which is run when the expected method is called, in addition
290             to generating the result value or exception.
291              
292             When invoked, the code body is invoked in void context with no additional
293             arguments.
294              
295             =cut
296              
297             sub will_also
298             {
299 3     3   6 my $self = shift;
300 3         5 push @{ $self->[ALSO] }, @_;
  3         19  
301              
302 3         8 return $self;
303             }
304              
305             sub _consume
306             {
307 13     13   21 my $self = shift;
308 13         18 my ( $method, @args ) = @_;
309              
310 13 50       28 $method eq $self->[METHOD] or
311             return 0;
312              
313 13         31 my ( $ok, $stack ) = Test::Deep::cmp_details( \@args, $self->[ARGS] );
314 13 100       23066 unless( $ok ) {
315 1         5 $self->[DIAG] = Test::Deep::deep_diag( $stack );
316 1         153 return 0;
317             }
318              
319 12         21 $self->[CALLED]++;
320 12         60 return 1;
321             }
322              
323             sub _check
324             {
325 14     14   20 my $self = shift;
326 14         18 my ( $builder ) = @_;
327              
328 14         23 my $method = $self->[METHOD];
329 14         24 $builder->ok( $self->[CALLED], "->$method(${\ Test::ExpectAndCheck::_stringify_args @{ $self->[ARGS] } })" );
  14         18  
  14         32  
330 14 100       4853 $builder->diag( $self->[DIAG] ) if defined $self->[DIAG];
331             }
332              
333             sub _result
334             {
335 12     12   17 my $self = shift;
336              
337 12 100       27 if( my $also = $self->[ALSO] ) {
338 3         13 $_->() for @$also;
339             }
340              
341 12 100       45 die $self->[THROWS] if defined $self->[THROWS];
342 11 100       22 return unless $self->[RETURNS];
343 10 50       17 return @{ $self->[RETURNS] } if wantarray;
  0         0  
344 10         62 return $self->[RETURNS][0];
345             }
346              
347             sub _called
348             {
349 13     13   17 my $self = shift;
350 13         49 return $self->[CALLED];
351             }
352              
353             sub _stringify
354             {
355 1     1   3 my $self = shift;
356 1         3 return "->$self->[METHOD](${\( Test::ExpectAndCheck::_stringify_args @{ $self->[ARGS] } )}) at $self->[FILE] line $self->[LINE]";
  1         3  
  1         3  
357             }
358              
359             package
360             Test::ExpectAndCheck::_Obj;
361              
362             our @CARP_NOT = qw( Test::ExpectAndCheck );
363              
364             sub new
365             {
366 2     2   2 my $class = shift;
367 2         6 my ( $controller ) = @_;
368              
369 2         5 return bless [ $controller ], $class;
370             }
371              
372             sub AUTOLOAD
373             {
374 16     16   5052 my $self = shift;
375 16         94 ( our $AUTOLOAD ) =~ m/::([^:]+)$/;
376 16         34 my $method = $1;
377              
378 16 100       264 return if $method eq "DESTROY";
379              
380 14         36 return $self->[0]->_call( $method, @_ );
381             }
382              
383             =head1 AUTHOR
384              
385             Paul Evans
386              
387             =cut
388              
389             0x55AA;