File Coverage

blib/lib/Test/Class/Tiny.pm
Criterion Covered Total %
statement 106 118 89.8
branch 34 50 68.0
condition 3 3 100.0
subroutine 13 13 100.0
pod 1 4 25.0
total 157 188 83.5


line stmt bran cond sub pod time code
1             package Test::Class::Tiny;
2              
3 5     5   1500362 use strict;
  5         12  
  5         276  
4 5     5   118 use warnings;
  5         17  
  5         686  
5              
6             our $VERSION;
7             $VERSION = '0.03';
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 5     5   42 use mro ();
  5         13  
  5         209  
237              
238 5     5   33 use Test2::API ();
  5         14  
  5         330  
239              
240             our ($a, $b);
241              
242             #----------------------------------------------------------------------
243              
244 5     5   45 use constant SKIP_CLASS => ();
  5         11  
  5         6776  
245              
246 5     5 0 265205 sub new { bless {}, shift }
247              
248             sub num_tests {
249 2     2 1 323 my ($self) = @_;
250              
251 2 50       12 if (!$self->{'_running'}) {
252 0         0 die "num_tests() called outside of running test!";
253             }
254              
255 2         16 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         9 $self->{'test'}{$name}{'count'} = $count;
268 1         4 $self->{'test'}{$name}{'simple_name'} = $name;
269              
270 1         3 return $self;
271             }
272              
273             sub runtests {
274 5     5 0 1136166 my ($self) = @_;
275              
276 5 100       28 if (!ref $self) {
277 4         44 $self = $self->new();
278             }
279              
280 5         37 local $self->{'_running'} = 1;
281              
282             # Allow calls as either instance or object method.
283 5 50       24 if (!ref $self) {
284 0         0 my $obj = $self->new();
285 0         0 $self = $obj;
286             }
287              
288 5         31 my $big_ctx = Test2::API::context();
289 5         17773 my $ctx = $big_ctx->snapshot();
290 5         161 $big_ctx->release();
291              
292 5 50       326 if (my $reason = $self->SKIP_CLASS()) {
293 0         0 $ctx->plan(1);
294 0         0 $ctx->skip( ref($self), $reason );
295             }
296             else {
297 5         57 $self->_analyze();
298              
299 5 100       26 if ( my $startup_hr = $self->{'startup'} ) {
300 1         4 $self->_run_funcs($startup_hr);
301             }
302              
303 5 50       71 if ( my $tests_hr = $self->{'test'} ) {
304 5         16 my $setup_hr = $self->{'setup'};
305 5         27 my $teardown_hr = $self->{'teardown'};
306              
307 5         13 my $filter_fn;
308             my $got_count;
309              
310 5         33 my $hub = $ctx->hub();
311              
312 5 50       62 $hub->plan('NO PLAN') if !defined $hub->plan();
313              
314             my $filter_cr = sub {
315 26     26   40022 my ($hub, $event) = @_;
316              
317 26 100       147 $got_count++ if $event->increments_count();
318              
319 26 100 100     333 if ($event->can('name') && !defined $event->name()) {
320 10         85 my $name = $tests_hr->{$filter_fn}{'simple_name'};
321 10         30 $name =~ tr<_>< >;
322 10         50 $event->set_name($name);
323             }
324              
325 26         186 return $event;
326 5         200 };
327              
328 5         32 $hub->filter($filter_cr);
329              
330             my @sorted_fns = sort {
331 5 50       256 ( $tests_hr->{$a}{'simple_name'} cmp $tests_hr->{$b}{'simple_name'} )
  20         99  
332             || ( $a cmp $b )
333             } keys %$tests_hr;
334              
335 5         19 for my $fn (@sorted_fns) {
336 17         67 $filter_fn = $fn;
337              
338 17 50       66 if (my $ptn = $ENV{'TEST_METHOD'}) {
339 0 0       0 next if $fn !~ m<$ptn>;
340             }
341              
342 17 50       51 if ($ENV{'TEST_VERBOSE'}) {
343 0         0 $ctx->diag( $/ . ref($self) . "->$fn()" );
344             }
345              
346 17         72 $self->_run_funcs($setup_hr);
347              
348 17         27 $got_count = 0;
349              
350 17         80 my $want_count = $tests_hr->{$fn}{'count'};
351              
352 17         51 local $self->{'_num_tests'} = $want_count;
353              
354 17         35 local $@;
355 17 50       35 eval { $self->$fn(); 1 } or do {
  17         118  
  17         5692  
356 0         0 my $err = $@;
357 0         0 $ctx->fail("$fn()", "Caught exception: $err");
358             };
359              
360 17 100       123 if ($want_count) {
361 15 50       57 if ($want_count != $got_count) {
362 0         0 $ctx->fail("Test count mismatch: got $got_count, expected $want_count");
363             }
364             }
365              
366 17         99 $self->_run_funcs($teardown_hr);
367             }
368              
369 5         35 $hub->unfilter($filter_cr);
370             }
371              
372 5 100       230 if ( my $shutdown_hr = $self->{'shutdown'} ) {
373 1         4 $self->_run_funcs($shutdown_hr);
374             }
375             }
376              
377 5         26 return;
378             }
379              
380             sub _analyze {
381 5     5   19 my ($self) = @_;
382              
383 5 50       24 if (!$self->{'_analyzed'}) {
384 5         12 my @isa = @{ mro::get_linear_isa(ref $self) };
  5         79  
385              
386 5         13 my $t_regexp = q;
387 5         405 my $prefix_regexp = qr<\A${t_regexp}_(.+)>;
388 5         240 my $suffix_regexp = qr<(.+)_$t_regexp\z>;
389              
390 5         26 for my $ns (@isa) {
391 12         32 my $ptbl_hr = do {
392 5     5   48 no strict 'refs';
  5         8  
  5         2221  
393 12         23 \%{"${ns}::"};
  12         76  
394             };
395              
396 12         518 for my $name (keys %$ptbl_hr) {
397 770 100       2642 next if !$self->can($name);
398              
399 733         1216 my ($whatsit, $simple_name);
400              
401 733 100       3648 if ($name =~ $prefix_regexp) {
    100          
402 11         35 $whatsit = $1;
403 11         27 $simple_name = $2;
404             }
405             elsif ($name =~ $suffix_regexp) {
406 9         26 $simple_name = $1;
407 9         21 $whatsit = $2;
408             }
409             else {
410 713         1403 next;
411             }
412              
413 20 100       73 if ( $whatsit =~ s<_><> ) {
414 4         24 $self->{$whatsit}{$name} = undef;
415             }
416             else {
417 16         123 $self->{'test'}{$name} = {
418             count => $whatsit,
419             simple_name => $simple_name,
420             };
421             }
422             }
423             }
424              
425 5         30 $self->{'_analyzed'} = 1;
426             }
427              
428 5         15 return;
429             }
430              
431             sub _run_funcs {
432 36     36   87 my ($self, $funcs_hr) = @_;
433              
434 36         139 for my $fn (sort keys %$funcs_hr) {
435 12 50       35 if ( $funcs_hr->{$fn} ) {
436 0         0 $funcs_hr->{$fn}->($self);
437             }
438             else {
439 12         47 $self->$fn();
440             }
441             }
442              
443 36         205 return;
444             }
445              
446             1;