File Coverage

blib/lib/Test/Class/Tiny.pm
Criterion Covered Total %
statement 98 117 83.7
branch 29 50 58.0
condition 3 3 100.0
subroutine 13 13 100.0
pod 1 4 25.0
total 144 187 77.0


line stmt bran cond sub pod time code
1             package Test::Class::Tiny;
2              
3 4     4   907492 use strict;
  4         24  
  4         108  
4 4     4   19 use warnings;
  4         10  
  4         272  
5              
6             our $VERSION;
7             $VERSION = '0.02_02';
8              
9             =encoding utf-8
10              
11             =head1 NAME
12              
13             Test::Class::Tiny - xUnit in Perl, simplified
14              
15             =head1 SYNOPSIS
16              
17             package t::mytest;
18              
19             use parent qw( Test::Class::Tiny );
20              
21             __PACKAGE__->runtests() if !caller;
22              
23             sub T_startup_something {
24             # Runs at the start of the test run.
25             }
26              
27             sub something_T_setup {
28             # Runs before each normal test function
29             }
30              
31             # Expects 2 assertions:
32             sub T2_normal {
33             ok(1, 'yes');
34             ok( !0, 'no');
35             }
36              
37             # Ignores assertion count:
38             sub T0_whatever {
39             ok(1, 'yes');
40             }
41              
42             sub T_teardown_something {
43             # Runs after each normal test function
44             }
45              
46             sub T_shutdown_something {
47             # Runs at the end of the test run.
48             }
49              
50             =head1 STATUS
51              
52             This module is B. If you use it, you MUST check the changelog
53             before upgrading to a new version. Any CPAN distributions that use this module
54             could break whenever this module is updated.
55              
56             =head1 DESCRIPTION
57              
58             L has served Perl’s xUnit needs for a long time
59             but is incompatible with the L framework. This module allows for
60             a similar workflow but in a way that works with both L and the older,
61             L-based modules.
62              
63             =head1 HOW (AND WHY) TO USE THIS MODULE
64              
65             xUnit encourages well-designed tests by encouraging organization of test
66             logic into independent chunks of test logic rather than a single monolithic
67             block of code.
68              
69             xUnit provides standard hooks for:
70              
71             =over
72              
73             =item * startup: The start of all tests
74              
75             =item * setup: The start of an individual test group (i.e., Perl function)
76              
77             =item * teardown: The end of an individual test group
78              
79             =item * shutdown: The end of all tests
80              
81             =back
82              
83             To write functions that execute at these points in the workflow,
84             name those functions with the prefixes C, C,
85             C, or C. B, name such functions
86             with the I C<_T_startup>, C<_T_setup>, C<_T_teardown>, or
87             C<_T_shutdown>.
88              
89             To write a test function—i.e., a function that actually runs some
90             assertions—prefix the function name with C, the number of test assertions
91             in the function, then an underscore. For example, a function that contains
92             9 assertions might be named C. If that function
93             doesn’t run exactly 9 assertions, a test failure is produced.
94              
95             To forgo counting test assertions, use 0 as the test count, e.g.,
96             C.
97              
98             You may alternatively use suffix-style naming for test functions well,
99             e.g., C, C.
100              
101             The above convention is a significant departure from L,
102             which uses Perl subroutine attributes to indicate this information.
103             Using method names is dramatically simpler to implement and also easier
104             to type.
105              
106             In most other respects this module attempts to imitate L.
107              
108             =head2 PLANS
109              
110             The concept of a global “plan” (i.e., an expected number of assertions)
111             isn’t all that sensible with xUnit because each test function has its
112             own plan. So, ideally the total number of expected assertions for a given
113             test module is just the sum of all test functions’ expected assertions.
114              
115             Thus, currently, C sets the L object’s plan to
116             C if the plan is undefined.
117              
118             =head1 TEST INHERITANCE
119              
120             Like L, this module seamlessly integrates inherited methods.
121             To have one test module inherit another module’s tests, just make that
122             first module a subclass of the latter.
123              
124             B Inheritance in tests, while occasionally useful, can also
125             make for difficult maintenance over time if overused. Where I’ve found it
126             most useful is cases like L, where each test needs to run with
127             each backend implementation.
128              
129             =head1 RUNNING YOUR TEST
130              
131             To use this module to write normal Perl test scripts, just define
132             the script’s package (ideally not C
, but it’ll work) as a subclass of
133             this module. Then put the following somewhere in the script:
134              
135             __PACKAGE__->runtests() if !caller;
136              
137             Your test will thus execute as a “modulino”.
138              
139             =head1 SPECIAL FEATURES
140              
141             =over
142              
143             =item * As in L, a C method may be defined. If this
144             method returns truthy, then the class’s tests are skipped, and that truthy
145             return is given as the reason for the skip.
146              
147             =item * The C environment variable is honored as in L.
148              
149             =item * L’s C method is NOT recognized
150             here because an early return will already trigger a failure.
151              
152             =item * Within a test method, C may be called to retrieve the
153             number of expected test assertions.
154              
155             =item * To define a test function whose test count isn’t known until runtime,
156             name it B the usual C prefix, then at runtime do:
157              
158             $test_obj->num_method_tests( $name, $count )
159              
160             See F in the distribution for an example of this.
161              
162             =back
163              
164             =head1 COMMON PITFALLS
165              
166             Avoid the following:
167              
168             =over
169              
170             =item * Writing startup logic outside of the module class, e.g.:
171              
172             if (!caller) {
173             my $mock = Test::MockModule->new('Some::Module');
174             $mock->redefine('somefunc', sub { .. } );
175              
176             __PACKAGE__->runtests();
177             }
178              
179             The above works I if the test module runs in its own process; if you try
180             to run this module with anything else it’ll fail because C will be
181             truthy, which will prevent the mocking from being set up, which your test
182             probably depends on.
183              
184             Instead of the above, write a wrapper around C, thus:
185              
186             sub runtests {
187             my $self = shift;
188              
189             my $mock = Test::MockModule->new('Some::Module');
190             $mock->redefine('somefunc', sub { .. } );
191              
192             $self->SUPER::runtests();
193             }
194              
195             This ensures your test module will always run with the intended mocking.
196              
197             =item * REDUX: Writing startup logic outside of the module class, e.g.:
198              
199             my $mock = Test::MockModule->new('Some::Module');
200             $mock->redefine('somefunc', sub { .. } );
201              
202             __PACKAGE__->runtests() if !caller;
203              
204             This is even worse than before because the mock will be global, which
205             will quietly apply it where we don’t intend. This produces
206             action-at-a-distance bugs, which can be notoriously hard to find.
207              
208             =back
209              
210             =head1 SEE ALSO
211              
212             Besides L, you might also look at the following:
213              
214             =over
215              
216             =item * L also implements xUnit for L but doesn’t
217             allow inheritance.
218              
219             =item * L works with L, but the L requirement
220             makes use in CPAN modules problematic.
221              
222             =back
223              
224             =head1 AUTHOR
225              
226             Copyright 2019 L (FELIPE)
227              
228             =head1 LICENSE
229              
230             This code is licensed under the same license as Perl itself.
231              
232             =cut
233              
234             #----------------------------------------------------------------------
235              
236 4     4   22 use mro ();
  4         8  
  4         49  
237              
238 4     4   15 use Test2::API ();
  4         16  
  4         163  
239              
240             our ($a, $b);
241              
242             #----------------------------------------------------------------------
243              
244 4     4   23 use constant SKIP_CLASS => ();
  4         6  
  4         3513  
245              
246 4     4 0 113 sub new { bless {}, shift }
247              
248             sub num_tests {
249 1     1 1 73 my ($self) = @_;
250              
251 1 50       5 if (!$self->{'_running'}) {
252 0         0 die "num_tests() called outside of running test!";
253             }
254              
255 1         5 return $self->{'_num_tests'};
256             }
257              
258             sub num_method_tests {
259 1     1 0 13 my ($self, $name, $count) = @_;
260              
261 1 50       5 die 'need name!' if !$name;
262              
263 1 50       5 if (@_ == 2) {
264 0         0 return $self->{'test'}{$name};
265             }
266              
267 1         6 $self->{'test'}{$name}{'count'} = $count;
268 1         3 $self->{'test'}{$name}{'simple_name'} = $name;
269              
270 1         3 return $self;
271             }
272              
273             sub runtests {
274 4     4 0 1530 my ($self) = @_;
275              
276 4 100       19 if (!ref $self) {
277 3         19 $self = $self->new();
278             }
279              
280 4         22 local $self->{'_running'} = 1;
281              
282             # Allow calls as either instance or object method.
283 4 50       17 if (!ref $self) {
284 0         0 my $obj = $self->new();
285 0         0 $self = $obj;
286             }
287              
288 4         19 my $ctx = Test2::API::context();
289              
290 4 50       11920 if (my $reason = $self->SKIP_CLASS()) {
291 0         0 $ctx->plan(1);
292 0         0 $ctx->skip( ref($self), $reason );
293             }
294             else {
295 4         42 $self->_analyze();
296              
297 4 50       22 if ( my $startup_hr = $self->{'startup'} ) {
298 0         0 $self->_run_funcs($startup_hr);
299             }
300              
301 4 50       16 if ( my $tests_hr = $self->{'test'} ) {
302 4         10 my $setup_hr = $self->{'setup'};
303 4         8 my $teardown_hr = $self->{'teardown'};
304              
305 4         16 my $filter_fn;
306             my $got_count;
307              
308 4         25 my $hub = $ctx->hub();
309              
310 4 50       44 $hub->plan('NO PLAN') if !defined $hub->plan();
311              
312             my $filter_cr = sub {
313 18     18   12477 my ($hub, $event) = @_;
314              
315 18 100       57 $got_count++ if $event->increments_count();
316              
317 18 100 100     161 if ($event->can('name') && !defined $event->name()) {
318 9         59 my $name = $tests_hr->{$filter_fn}{'simple_name'};
319 9         28 $name =~ tr<_>< >;
320 9         26 $event->set_name($name);
321             }
322              
323 18         97 return $event;
324 4         156 };
325              
326 4         22 $hub->filter($filter_cr);
327              
328             my @sorted_fns = sort {
329 4 50       102 ( $tests_hr->{$a}{'simple_name'} cmp $tests_hr->{$b}{'simple_name'} )
  13         50  
330             || ( $a cmp $b )
331             } keys %$tests_hr;
332              
333 4         31 for my $fn (@sorted_fns) {
334 12         24 $filter_fn = $fn;
335              
336 12 50       36 if (my $ptn = $ENV{'TEST_METHOD'}) {
337 0 0       0 next if $fn !~ m<$ptn>;
338             }
339              
340 12 50       27 if ($ENV{'TEST_VERBOSE'}) {
341 0         0 $ctx->diag( $/ . ref($self) . "->$fn()" );
342             }
343              
344 12         32 $self->_run_funcs($setup_hr);
345              
346 12         20 $got_count = 0;
347              
348 12         25 my $want_count = $tests_hr->{$fn}{'count'};
349              
350 12         30 local $self->{'_num_tests'} = $want_count;
351              
352 12         20 local $@;
353 12 50       17 eval { $self->$fn(); 1 } or do {
  12         62  
  12         1630  
354 0         0 my $err = $@;
355 0         0 $ctx->fail("$fn()", "Caught exception: $err");
356             };
357              
358 12 100       33 if ($want_count) {
359 11 50       30 if ($want_count != $got_count) {
360 0         0 $ctx->fail("Test count mismatch: got $got_count, expected $want_count");
361             }
362             }
363              
364 12         32 $self->_run_funcs($teardown_hr);
365             }
366              
367 4         21 $hub->unfilter($filter_cr);
368             }
369              
370 4 50       120 if ( my $shutdown_hr = $self->{'shutdown'} ) {
371 0         0 $self->_run_funcs($shutdown_hr);
372             }
373             }
374              
375 4         16 $ctx->release();
376              
377 4         121 return;
378             }
379              
380             sub _analyze {
381 4     4   11 my ($self) = @_;
382              
383 4 50       22 if (!$self->{'_analyzed'}) {
384 4         8 my @isa = @{ mro::get_linear_isa(ref $self) };
  4         33  
385              
386 4         29 my $t_regexp = q;
387 4         184 my $prefix_regexp = qr<\A${t_regexp}_(.+)>;
388 4         166 my $suffix_regexp = qr<(.+)_$t_regexp\z>;
389              
390 4         20 for my $ns (@isa) {
391 10         17 my $ptbl_hr = do {
392 4     4   38 no strict 'refs';
  4         9  
  4         1081  
393 10         15 \%{"${ns}::"};
  10         42  
394             };
395              
396 10         149 for my $name (keys %$ptbl_hr) {
397 595 100       1500 next if !$self->can($name);
398              
399 565         793 my ($whatsit, $simple_name);
400              
401 565 100       1877 if ($name =~ $prefix_regexp) {
    50          
402 11         27 $whatsit = $1;
403 11         20 $simple_name = $2;
404             }
405             elsif ($name =~ $suffix_regexp) {
406 0         0 $simple_name = $1;
407 0         0 $whatsit = $2;
408             }
409             else {
410 554         892 next;
411             }
412              
413 11 50       31 if ( $whatsit =~ s<_><> ) {
414 0         0 $self->{$whatsit}{$name} = undef;
415             }
416             else {
417 11         59 $self->{'test'}{$name} = {
418             count => $whatsit,
419             simple_name => $simple_name,
420             };
421             }
422             }
423             }
424              
425 4         22 $self->{'_analyzed'} = 1;
426             }
427              
428 4         10 return;
429             }
430              
431             sub _run_funcs {
432 24     24   43 my ($self, $funcs_hr) = @_;
433              
434 24         68 for my $fn (sort keys %$funcs_hr) {
435 0 0       0 if ( $funcs_hr->{$fn} ) {
436 0         0 $funcs_hr->{$fn}->($self);
437             }
438             else {
439 0         0 $self->$fn();
440             }
441             }
442              
443 24         62 return;
444             }
445              
446             1;