root/lang/perl/Test-Classy/trunk/lib/Test/Classy/Base.pm @ 15510

Revision 15510, 7.0 kB (checked in by charsbar, 5 years ago)

Test-Classy: added $class->test_name and 0.02 -> CPAN

  • Property svn:eol-style set to native
Line 
1package Test::Classy::Base;
2
3use strict;
4use warnings;
5use base qw( Class::Data::Inheritable );
6use Test::More ();
7use Class::Inspector;
8
9sub import {
10  my ($class, @flags)  = @_;
11  my $caller = caller;
12
13  if ( $class ne __PACKAGE__ ) {
14    return unless grep { $_ eq 'base' } @flags;
15  }
16
17  no strict 'refs';
18  push @{"$caller\::ISA"}, $class;
19
20  # XXX: not sure why but $TODO refused to be exported well
21  *{"$caller\::TODO"} = \$Test::More::TODO;
22
23  foreach my $export ( @Test::More::EXPORT ) {
24    next if $export =~ /^\W/;
25    *{"$caller\::$export"} = \&{"Test::More\::$export"};
26  }
27
28  if ( grep { $_ eq 'ignore' } @flags ) {
29    ${"$caller\::_ignore"} = 1;
30  }
31
32  if ( $class eq __PACKAGE__ ) {
33    $caller->mk_classdata( _tests => {} );
34    $caller->mk_classdata( _plan => 0 );
35    $caller->mk_classdata( test_name => '' );
36  }
37}
38
39sub MODIFY_CODE_ATTRIBUTES {
40  my ($class, $code, @attrs) = @_;
41
42  my %stash;
43  foreach my $attr ( @attrs ) {
44    if ( $attr eq 'Test' ) {
45      $stash{plan} = 1;
46    }
47    elsif ( my ($dummy, $plan) = $attr =~ /^Tests?\((['"]?)(\d+|no_plan)\1\)$/ ) {
48      $stash{plan} = $plan;
49    }
50    elsif ( my ($type, $dummy2, $reason) = $attr =~ /^(Skip|TODO)(?:\((['"]?)(.+)\)\2)?$/ ) {
51      $stash{$type} = $reason;
52    }
53    else {
54      $stash{$attr} = 1;
55    }
56  }
57  return unless $stash{plan};
58
59  if ( $stash{plan} eq 'no_plan' ) {
60    Test::More::plan 'no_plan' unless Test::More->builder->{Have_Plan};
61    $stash{plan} = 0;
62  }
63
64  $class->_plan( $class->_plan + $stash{plan} );
65
66  $stash{code} = $code;
67
68  # At this point, the name looks like CODE(...)
69  # we'll make it human-readable later, with class inspection
70  $class->_tests->{$code} = \%stash;
71
72  return;
73}
74
75sub _limit {
76  my ($class, @monikers) = @_;
77
78  my $tests = $class->_tests;
79  my $reason = 'tests only ' . ( join ', ', @monikers );
80
81LOOP:
82  foreach my $name ( keys %{ $tests } ) {
83    foreach my $moniker ( @monikers ) {
84      next LOOP if exists $tests->{$name}->{$moniker};
85    }
86    $tests->{$name}->{Skip} = $reason;
87  }
88}
89
90sub _should_be_ignored {
91  my $class = shift;
92
93  { no strict 'refs';
94    if ( ${"$class\::_ignore"} ) {
95      SKIP: {
96        Test::More::skip 'a base class, not to test', $class->_plan;
97      }
98      return 1;
99    }
100  }
101}
102
103sub _find_symbols {
104  my $class = shift;
105
106  my $methods = Class::Inspector->methods($class, 'expanded');
107
108  my %symbols;
109  foreach my $entry ( @{ $methods } ) {
110    $symbols{$entry->[3]} = $entry->[2];  # coderef to sub name
111  }
112  return %symbols;
113}
114
115sub _run_tests {
116  my ($class, @args) = @_;
117
118  return if $class->_should_be_ignored;
119
120  my %sym = $class->_find_symbols;
121
122  $class->initialize(@args);
123
124  my $tests = $class->_tests;
125
126  foreach my $name ( sort { $sym{$a} cmp $sym{$b} } grep { $sym{$_} } keys %{ $tests } ) {
127    next if $sym{$name} =~ /^(?:initialize|finalize)$/;
128
129    if ( my $reason = $class->_should_skip_the_rest ) {
130      SKIP: { Test::More::skip $reason, $tests->{$name}->{plan}; }
131      next;
132    }
133
134    $class->_run_test( $tests->{$name}, $sym{$name}, @args );
135  }
136
137  $class->finalize(@args);
138}
139
140sub _run_test {
141  my ($class, $test, $name, @args) = @_;
142
143  $class->test_name( $name );
144
145  if ( exists $test->{TODO} ) {
146    my $reason = defined $test->{TODO}
147      ? $test->{TODO}
148      : "$name is not implemented";
149
150    if ( exists $test->{Skip} ) {  # todo skip
151      TODO: {
152        Test::More::todo_skip $reason, $test->{plan};
153      }
154    }
155    else {
156      TODO: {
157        no strict 'refs';
158        local ${"$class\::TODO"} = $reason; # perl 5.6.2 hates this
159        $test->{code}($class, @args);
160      }
161    }
162    return;
163  }
164  elsif ( exists $test->{Skip} ) {
165    my $reason = defined $test->{Skip}
166      ? $test->{Skip}
167      : "skipped $name";
168    SKIP: { Test::More::skip $reason, $test->{plan}; }
169    return;
170  }
171
172  $test->{code}($class, @args);
173}
174
175sub skip_the_rest {
176  my ($class, $reason) = @_;
177
178  no strict 'refs';
179  ${"$class\::_skip_the_rest"} = $reason || 'for some reason';
180}
181
182sub _should_skip_the_rest {
183  my $class = shift;
184
185  no strict 'refs';
186  return ${"$class\::_skip_the_rest"};
187}
188
189sub initialize {}
190sub finalize {}
191
1921;
193
194__END__
195
196=head1 NAME
197
198Test::Classy::Base
199
200=head1 SYNOPSIS
201
202  package MyApp::Test::ForSomething;
203  use Test::Classy::Base;
204
205  __PACKAGE__->mk_classdata('model');
206
207  sub initialize {
208    my $class = shift;
209
210    eval { require 'Some::Model'; };
211    $class->skip_the_rest('Some::Model is required') if $@;
212
213    my $model = Some::Model->connect;
214
215    $class->model($model);
216  }
217
218  sub mytest : Test {
219    my $class = shift;
220    ok $class->model->find('something'), $class->test_name." works";
221  }
222
223  sub finalize {
224    my $class = shift;
225    $class->model->disconnect if $class->model;
226    $class->model(undef);
227  }
228
229=head1 DESCRIPTION
230
231This is a base class for actual tests. See L<Test::Classy> for basic usage.
232
233=head1 CLASS METHODS
234
235=head2 skip_the_rest
236
237If you called this with a reason why you want to skip (unsupported OS or lack of modules, for example), all the remaining tests in the package will be skipped.
238
239=head2 initialize
240
241This is called before the tests runs. You might want to set up database or something like that here. You can store initialized thingy as a class data (via Class::Data::Inheritable), or as a package-wide variable, maybe. Note that you can set up thingy in a test script and pass it as an argument for each of the tests instead.
242
243=head2 finalize
244
245This method is (hopefully) called when all the tests in the package are done. You might also want provide END/DESTROY to clean up thingy when the tests should be bailed out.
246
247=head2 test_name
248
249returns the name of the test running currently. Handy to write a meaningful test message.
250
251=head1 NOTES FOR INHERITING TESTS
252
253You may want to let tests inherit some base class (especially to reuse common initialization/finalization). You can use good old base.pm (or parent.pm) to do this, though you'll need to use Test::More and the likes explicitly as base.pm doesn't export things:
254
255  package MyApp::Test::Base;
256  use Test::Classy::Base;
257  use MyApp::Model;
258
259  __PACKAGE__->mk_classdata('model');
260
261  sub initialize {
262    my $class = shift;
263
264    $class->model( MyApp::Model->new );
265  }
266
267  package MyApp::Test::Specific;
268  use base qw( MyApp::Test::Base );
269  use Test::More;  # you'll need this.
270
271  sub test : Test { ok shift->model->does_fine; }
272
273You also can add 'base' option while using your base class. In this case, all the methods will be exported.
274
275  package MyApp::Test::Specific;
276  use MyApp::Test::Base 'base';
277
278  sub test : Test { ok shift->model->does_fine; }
279
280When your base class has some common tests to be inherited, and you don't want them to be tested in the base class, add 'ignore' option when you use Test::Classy::Base:
281
282  package MyApp::Test::AnotherBase;
283  use Test::Classy::Base 'ignore';
284
285  sub not_for_base : Test { pass 'for children only' };
286
287=head1 AUTHOR
288
289Kenichi Ishigaki, E<lt>ishigaki@cpan.orgE<gt>
290
291=head1 COPYRIGHT AND LICENSE
292
293Copyright (C) 2008 by Kenichi Ishigaki.
294
295This program is free software; you can redistribute it and/or
296modify it under the same terms as Perl itself.
297
298=cut
Note: See TracBrowser for help on using the browser.