File Coverage

blib/lib/Test/ExpectAndCheck.pm
Criterion Covered Total %
statement 115 124 92.7
branch 26 34 76.4
condition 4 6 66.6
subroutine 27 27 100.0
pod 3 3 100.0
total 175 194 90.2


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   64203 use strict;
  3         12  
  3         75  
9 3     3   13 use warnings;
  3         4  
  3         103  
10              
11             our $VERSION = '0.03';
12              
13 3     3   14 use Carp;
  3         4  
  3         160  
14              
15 3     3   16 use List::Util qw( first );
  3         5  
  3         254  
16 3     3   16 use Scalar::Util qw( blessed );
  3         4  
  3         116  
17              
18 3     3   1678 use Test::Deep ();
  3         25066  
  3         97  
19              
20 3     3   27 use constant EXPECTATION_CLASS => "Test::ExpectAndCheck::_Expectation";
  3         6  
  3         2339  
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, $puppet ) = Test::ExpectAndCheck->create;
32              
33             {
34             $controller->expect( act => 123, 45 )
35             ->returns( 678 );
36              
37             is( $puppet->act( 123, 45 ), 678, '$puppet->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 mocked "puppet" instance will expect to receive a given
48             list of 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, $puppet ) = 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 "puppet", 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 174 my $class = shift;
74              
75 2         23 my $controller = bless {
76             expectations => [],
77             }, $class;
78 2         93 my $puppet = Test::ExpectAndCheck::_Obj->new( $controller );
79              
80 2         9 return ( $controller, $puppet );
81             }
82              
83             =head2 expect
84              
85             $exp = $controller->expect( $method, @args )
86              
87             Specifies that the puppet 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 10     10 1 19958 my $self = shift;
102 10         36 my ( $method, @args ) = @_;
103              
104 10         29 my ( undef, $file, $line ) = caller(1);
105 10 50       114 defined $file or ( undef, $file, $line ) = caller(0);
106              
107 10         28 push @{ $self->{expectations} }, my $exp = $self->EXPECTATION_CLASS->new(
  10         157  
108             $method => [ @args ], $file, $line,
109             );
110              
111 10         55 return $exp;
112             }
113              
114             sub _stringify
115             {
116 8     8   28 my ( $v ) = @_;
117 8 50 33     164 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         399 return sprintf "%d", $v;
125             }
126             elsif( $v =~ m/^[\x20-\x7E]*\z/ ) {
127 6         30 $v =~ s/([\\'])/\\$1/g;
128 6         71 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 13     13   56 join ", ", map { _stringify $_ } @_;
  8         27  
146             }
147              
148             sub _call
149             {
150 10     10   22 my $self = shift;
151 10         24 my ( $method, @args ) = @_;
152              
153 10         14 my $e;
154 9     9   40 $e = first { !$_->_called } @{ $self->{expectations} } and
  10         72  
155 10 100 100     71 $e->_consume( $method, @args ) or do {
156 2         8 my $message = Carp::shortmess( "Unexpected call to ->$method(${\ _stringify_args @args })" );
  2         8  
157 2 100       87 $message .= "... while expecting " . $e->_stringify if $e;
158 2 100       9 $message .= "... after all expectations done" if !$e;
159 2         27 die "$message.\n";
160             };
161              
162 8         71 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 11     11 1 4093 my $self = shift;
178 11         31 my ( $name ) = @_;
179              
180 11         41 my $builder = Test::Builder->new;
181 11         70 local $Test::Builder::Level = $Test::Builder::Level + 1;
182              
183             $builder->subtest( $name, sub {
184 11     11   8505 my $count = 0;
185 11         23 foreach my $exp ( @{ $self->{expectations} } ) {
  11         43  
186 10         52 $exp->_check( $builder );
187 10         245 $count++;
188             }
189              
190 11 100       47 $builder->ok( 1, "No calls made" ) if !$count;
191 11         105 });
192              
193 11         12530 undef @{ $self->{expectations} };
  11         90  
194             }
195              
196             package
197             Test::ExpectAndCheck::_Expectation;
198              
199 3     3   24 use List::Util qw( all );
  3         7  
  3         268  
200              
201             use constant {
202 3         2072 METHOD => 0,
203             ARGS => 1,
204             FILE => 2,
205             LINE => 3,
206             CALLED => 4,
207             RETURNS => 5,
208             THROWS => 6,
209             DIAG => 7,
210 3     3   26 };
  3         11  
211              
212             =head1 EXPECTATIONS
213              
214             Each value returned by the L method is an "expectation", an object
215             that represents one expected method call, the arguments it should receive, and
216             the return value it should provide.
217              
218             =cut
219              
220             sub new
221             {
222 10     10   26 my $class = shift;
223 10         32 my ( $method, $args, $file, $line ) = @_;
224 10         42 return bless [ $method, $args, $file, $line, 0 ], $class;
225             }
226              
227             =head2 returns
228              
229             $exp->returns( @result )
230              
231             Sets the result that will be returned by this method call.
232              
233             =cut
234              
235             sub returns
236             {
237 3     3   9 my $self = shift;
238              
239 3         8 $self->[RETURNS] = [ @_ ];
240 3         7 undef $self->[THROWS];
241              
242 3         12 return $self;
243             }
244              
245             =head2 throws
246              
247             $exp->throws( $e )
248              
249             Sets the exception that will be thrown by this method call.
250              
251             =cut
252              
253             sub throws
254             {
255 1     1   3 my $self = shift;
256 1         3 ( $self->[THROWS] ) = @_;
257              
258 1         3 return $self;
259             }
260              
261             sub _consume
262             {
263 9     9   19 my $self = shift;
264 9         26 my ( $method, @args ) = @_;
265              
266 9 50       32 $method eq $self->[METHOD] or
267             return 0;
268              
269 9         57 my ( $ok, $stack ) = Test::Deep::cmp_details( \@args, $self->[ARGS] );
270 9 100       21973 unless( $ok ) {
271 1         6 $self->[DIAG] = Test::Deep::deep_diag( $stack );
272 1         169 return 0;
273             }
274              
275 8         23 $self->[CALLED]++;
276 8         69 return 1;
277             }
278              
279             sub _check
280             {
281 10     10   22 my $self = shift;
282 10         21 my ( $builder ) = @_;
283              
284 10         27 my $method = $self->[METHOD];
285 10         35 $builder->ok( $self->[CALLED], "->$method(${\ Test::ExpectAndCheck::_stringify_args @{ $self->[ARGS] } })" );
  10         19  
  10         47  
286 10 100       4293 $builder->diag( $self->[DIAG] ) if defined $self->[DIAG];
287             }
288              
289             sub _result
290             {
291 3     3   5 my $self = shift;
292 3 100       22 die $self->[THROWS] if defined $self->[THROWS];
293 2 100       11 return unless $self->[RETURNS];
294 1 50       3 return @{ $self->[RETURNS] } if wantarray;
  0         0  
295 1         8 return $self->[RETURNS][0];
296             }
297              
298             sub _called
299             {
300 9     9   22 my $self = shift;
301 9         70 return $self->[CALLED];
302             }
303              
304             sub _stringify
305             {
306 1     1   2 my $self = shift;
307 1         4 return "->$self->[METHOD](${\( Test::ExpectAndCheck::_stringify_args @{ $self->[ARGS] } )}) at $self->[FILE] line $self->[LINE]";
  1         2  
  1         6  
308             }
309              
310             package
311             Test::ExpectAndCheck::_Obj;
312              
313             our @CARP_NOT = qw( Test::ExpectAndCheck );
314              
315             sub new
316             {
317 2     2   6 my $class = shift;
318 2         6 my ( $controller ) = @_;
319              
320 2         7 return bless [ $controller ], $class;
321             }
322              
323             sub AUTOLOAD
324             {
325 12     12   4778 my $self = shift;
326 12         106 ( our $AUTOLOAD ) =~ m/::([^:]+)$/;
327 12         37 my $method = $1;
328              
329 12 100       295 return if $method eq "DESTROY";
330              
331 10         60 return $self->[0]->_call( $method, @_ );
332             }
333              
334             =head1 AUTHOR
335              
336             Paul Evans
337              
338             =cut
339              
340             0x55AA;