After absorbing the information in Organizing Test Suites with Test::Class and Reusing Test Code with Test::Class, you're probably beginning
to understand how Test::Class
can make managing large codebases
easier. If you've worked with test cases before, you've likely realized that
test code is still code. Well-organized test code is easier to work with than
poorly organized test code.
Auto-discovering your test classes
There's too much repetitive boilerplate in these tests. We can make them easier. The first problem is the helper script, t/run.t:
#!/usr/bin/env perl -T
use lib 't/tests';
use Test::Person;
use Test::Person::Employee;
Test::Class->runtests;
Right now, this doesn't look so bad, but as you start to add more classes, this gets to be unwieldy. What if you forget to add a test class? Your class might be broken, but if the test class does not run, how will you know? Autodiscovering test classes helps:
#!/usr/bin/env perl -T
use Test::Class::Load qw<t/tests>;
Test::Class->runtests;
Tell Test::Class::Load
(bundled with Test::Class
) which directories your test classes are
in and it will find them for you. It does this by loading attempting to load
all files with a .pm
extension, so keep any helper test modules
(which are not Test::Class
tests) in a separate directory.
Using a common base class
Another useful technique of programming in general is to factor out common code. I've demonstrated this already, but there's room for improvement. Both test classes have a method for returning the name of the class being tested. It's possible to compute the name of this class, so why not push this into a base class? Add this to t/tests/My/Test/Class.pm:
package My::Test::Class;
use Test::Most;
use base qw<Test::Class Class::Data::Inheritable>;
BEGIN {
__PACKAGE__->mk_classdata('class');
}
sub startup : Tests( startup => 1 ) {
my $test = shift;
( my $class = ref $test ) =~ s/^Test:://;
return ok 1, "$class loaded" if $class eq __PACKAGE__;
use_ok $class or die;
$test->class($class);
}
1;
In Person::Employee
, delete the class
method.
In Person
, delete the class
and
startup
methods, and inherit from My::Test::Class
instead of Test::Class
. Now, class
will always
return the current class under testing. The new Test::Person
class looks like:
package Test::Person;
use Test::Most;
use base 'My::Test::Class';
sub constructor : Tests(3) {
my $test = shift;
my $class = $test->class;
can_ok $class, 'new';
ok my $person = $class->new, '... and the constructor should succeed';
isa_ok $person, $class, '... and the object it returns';
}
sub first_name : Tests(3) {
my $test = shift;
my $person = $test->class->new;
can_ok $person, 'first_name';
ok !defined $person->first_name,
'... and first_name should start out undefined';
$person->first_name('John');
is $person->first_name, 'John', '... and setting its value should succeed';
}
sub last_name : Tests(3) {
my $test = shift;
my $person = $test->class->new;
can_ok $person, 'last_name';
ok !defined $person->last_name,
'... and last_name should start out undefined';
$person->last_name('Public');
is $person->last_name, 'Public', '... and setting its value should succeed';
}
sub full_name : Tests(4) {
my $test = shift;
$test->_full_name_validation;
my $person = $test->class->new(
first_name => 'John',
last_name => 'Public',
);
is $person->full_name, 'John Public',
'... and setting its value should succeed';
}
sub _full_name_validation {
my ( $test, $person ) = @_;
my $person = $test->class->new;
can_ok $person, 'full_name';
throws_ok { $person->full_name }
qr/^Both first and last names must be set/,
'... and full_name() should croak() if the either name is not set';
$person->first_name('John');
throws_ok { $person->full_name }
qr/^Both first and last names must be set/,
'... and full_name() should croak() if the either name is not set';
}
1;
The test results for Test::Person::Employee
are:
All tests successful.
Files=1, Tests=32, 1 wallclock secs ( 0.33 cusr + 0.08 csys = 0.41 CPU)
There's an extra test, due to the ok 1
found in the
My::Test::Class::startup
method. It gets called an extra time
for the loading of My::Test::Class
.
Tip: If you must load your at BEGIN
time,
override this startup
method in your test class -- but be sure to
provide a class
method.
Run individual test classes
When I develop tests, I hate to leave my editor merely to run tests from the command line. To avoid this, I a mapping in my .vimrc file similar to:
noremap ,t :!prove --merge -lv %<CR>
When writing tests, I hit ,t
and my test runs. However,
doing this in a test class doesn't work. The class gets loaded, but the
tests do not run. I could add a new mapping:
noremap ,T :!prove -lv --merge t/run.t<CR>
... but this runs all of my test classes. If I have several hundred tests, I don't want to hunt back through all of the test output to see which tests failed. Instead, I want to run a single test class. I altered my mapping to include the path to my test classes.
noremap ,t :!prove -lv --merge -It/tests %<CR>
I also removed the Test::Class->runtests
line from
t/run.t (or else I'll have my tests run twice if I run the full test
suite). Because I use a common base class, I added a line to
My::Test::Class
:
INIT { Test::Class->runtests }
Regardless of whether I'm in a standard Test::Most
test
program or one of my new test classes, I can type ,t
and run
only the tests in the file I'm editing.
If you run the tests for Test::Person::Employee
, you'll see
the full run of 32 tests because Test::Class
will run the
tests for the current class and all classes from which it inherits. If you
run the tests for Test::Person
, you'll only see 15 tests run
-- the desired behavior.
If you prefer Emacs, add this to your ~/.emacs file:
(eval-after-load "cperl-mode"
'(add-hook 'cperl-mode-hook
(lambda () (local-set-key "\C-ct" 'cperl-prove))))
(defun cperl-prove ()
"Run the current test."
(interactive)
(shell-command (concat "prove -lv --merge -It/tests "
(shell-quote_argument (buffer-file-name)))))
That will bind this to C-c t
and you can pretend that
you're as cool as Vim users (just kidding! Stop the hate mail already).
Next time, learn to use test control methods with Test::Class.
I've been enjoying this so far. It's given me a lot of ideas for better approaches to test writing. How many of these does Ovid have planned? And do you have more contributors lined up?
There are two more in this series, Brian. I don't have more contributors yet, but I'm always happy to find them.