File Coverage

blib/lib/Test/Class/Tiny.pm
Criterion Covered Total %
statement 99 118 83.9
branch 29 50 58.0
condition 3 3 100.0
subroutine 13 13 100.0
pod 1 4 25.0
total 145 188 77.1


line stmt bran cond sub pod time code
1             package Test::Class::Tiny;
2              
3 4     4   1008168 use strict;
  4         25  
  4         119  
4 4     4   23 use warnings;
  4         9  
  4         344  
5              
6             our $VERSION;
7             $VERSION = '0.03_01';
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   29 use mro ();
  4         10  
  4         65  
237              
238 4     4   20 use Test2::API ();
  4         5  
  4         215  
239              
240             our ($a, $b);
241              
242             #----------------------------------------------------------------------
243              
244 4     4   29 use constant SKIP_CLASS => ();
  4         8  
  4         4308  
245              
246 4     4 0 106 sub new { bless {}, shift }
247              
248             sub num_tests {
249 1     1 1 107 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         6 return $self->{'_num_tests'};
256             }
257              
258             sub num_method_tests {
259 1     1 0 10 my ($self, $name, $count) = @_;
260              
261 1 50       5 die 'need name!' if !$name;
262              
263 1 50       4 if (@_ == 2) {
264 0         0 return $self->{'test'}{$name};
265             }
266              
267 1         7 $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 1809 my ($self) = @_;
275              
276 4 100       17 if (!ref $self) {
277 3         17 $self = $self->new();
278             }
279              
280 4         23 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         16 my $big_ctx = Test2::API::context();
289 4         12200 my $ctx = $big_ctx->snapshot();
290 4         96 $big_ctx->release();
291              
292 4 50       161 if (my $reason = $self->SKIP_CLASS()) {
293 0         0 $ctx->plan(1);
294 0         0 $ctx->skip( ref($self), $reason );
295             }
296             else {
297 4         29 $self->_analyze();
298              
299 4 50       20 if ( my $startup_hr = $self->{'startup'} ) {
300 0         0 $self->_run_funcs($startup_hr);
301             }
302              
303 4 50       17 if ( my $tests_hr = $self->{'test'} ) {
304 4         8 my $setup_hr = $self->{'setup'};
305 4         10 my $teardown_hr = $self->{'teardown'};
306              
307 4         9 my $filter_fn;
308             my $got_count;
309              
310 4         23 my $hub = $ctx->hub();
311              
312 4 50       34 $hub->plan('NO PLAN') if !defined $hub->plan();
313              
314             my $filter_cr = sub {
315 18     18   15484 my ($hub, $event) = @_;
316              
317 18 100       68 $got_count++ if $event->increments_count();
318              
319 18 100 100     171 if ($event->can('name') && !defined $event->name()) {
320 9         66 my $name = $tests_hr->{$filter_fn}{'simple_name'};
321 9         40 $name =~ tr<_>< >;
322 9         26 $event->set_name($name);
323             }
324              
325 18         116 return $event;
326 4         138 };
327              
328 4         22 $hub->filter($filter_cr);
329              
330             my @sorted_fns = sort {
331 4 50       107 ( $tests_hr->{$a}{'simple_name'} cmp $tests_hr->{$b}{'simple_name'} )
  12         48  
332             || ( $a cmp $b )
333             } keys %$tests_hr;
334              
335 4         15 for my $fn (@sorted_fns) {
336 12         46 $filter_fn = $fn;
337              
338 12 50       54 if (my $ptn = $ENV{'TEST_METHOD'}) {
339 0 0       0 next if $fn !~ m<$ptn>;
340             }
341              
342 12 50       39 if ($ENV{'TEST_VERBOSE'}) {
343 0         0 $ctx->diag( $/ . ref($self) . "->$fn()" );
344             }
345              
346 12         40 $self->_run_funcs($setup_hr);
347              
348 12         19 $got_count = 0;
349              
350 12         36 my $want_count = $tests_hr->{$fn}{'count'};
351              
352 12         30 local $self->{'_num_tests'} = $want_count;
353              
354 12         23 local $@;
355 12 50       20 eval { $self->$fn(); 1 } or do {
  12         58  
  12         1909  
356 0         0 my $err = $@;
357 0         0 $ctx->fail("$fn()", "Caught exception: $err");
358             };
359              
360 12 100       37 if ($want_count) {
361 11 50       31 if ($want_count != $got_count) {
362 0         0 $ctx->fail("Test count mismatch: got $got_count, expected $want_count");
363             }
364             }
365              
366 12         33 $self->_run_funcs($teardown_hr);
367             }
368              
369 4         22 $hub->unfilter($filter_cr);
370             }
371              
372 4 50       130 if ( my $shutdown_hr = $self->{'shutdown'} ) {
373 0         0 $self->_run_funcs($shutdown_hr);
374             }
375             }
376              
377 4         16 return;
378             }
379              
380             sub _analyze {
381 4     4   13 my ($self) = @_;
382              
383 4 50       18 if (!$self->{'_analyzed'}) {
384 4         25 my @isa = @{ mro::get_linear_isa(ref $self) };
  4         32  
385              
386 4         21 my $t_regexp = q;
387 4         213 my $prefix_regexp = qr<\A${t_regexp}_(.+)>;
388 4         141 my $suffix_regexp = qr<(.+)_$t_regexp\z>;
389              
390 4         18 for my $ns (@isa) {
391 10         17 my $ptbl_hr = do {
392 4     4   33 no strict 'refs';
  4         8  
  4         1217  
393 10         17 \%{"${ns}::"};
  10         47  
394             };
395              
396 10         173 for my $name (keys %$ptbl_hr) {
397 595 100       1538 next if !$self->can($name);
398              
399 565         857 my ($whatsit, $simple_name);
400              
401 565 100       2138 if ($name =~ $prefix_regexp) {
    50          
402 11         35 $whatsit = $1;
403 11         24 $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         960 next;
411             }
412              
413 11 50       35 if ( $whatsit =~ s<_><> ) {
414 0         0 $self->{$whatsit}{$name} = undef;
415             }
416             else {
417 11         50 $self->{'test'}{$name} = {
418             count => $whatsit,
419             simple_name => $simple_name,
420             };
421             }
422             }
423             }
424              
425 4         24 $self->{'_analyzed'} = 1;
426             }
427              
428 4         11 return;
429             }
430              
431             sub _run_funcs {
432 24     24   44 my ($self, $funcs_hr) = @_;
433              
434 24         82 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         74 return;
444             }
445              
446             1;