Cleaning up the Test::Class::Moose base class
I'm quite enjoying Test::Class::Moose
. It's very easy to use and it gives you such fine-grained control over your test suite and powerful reporting capabilities that it's turning out to be far more powerful than I had expected. It's actually easy enough to use for beginners, but power users will really appreciate it. There was, however, a major issue I had with it and it stems from a habit I picked up from Test::Class
.
For those who are very familiar with using Test::Class
(or if you've read my Test::Class tutorial), you may be used to seeing a base class that looks like this:
package My::Test::Class;
use parent 'Test::Class';
INIT { Test::Class->runtests }
sub startup : Tests(startup) {}
sub setup : Tests(setup) {}
sub teardown : Tests(teardown) {}
sub shutdown : Tests(shutdown) {}
1;
The empty test control methods are there so that a subclass knows it can always safely do this:
sub startup : Tests(startup) {
my $test = shift;
$test->next::method;
# more code here
}
Those stub test methods are no longer needed with Test::Class::Moose, but until today, that INIT
block was an annoying code smell that had some unfortunate side-effects. Let's make that go away.
The INIT Phase
First of all, what does that INIT
block do?
INIT { Test::Class->runtests }
In Test::Class
, that would simply run all loaded test classes at INIT
time. However, that means you could do this:
prove -lv t/lib/path/to/my/test/class.pm
And voilà, you test class could be executed just like it's a *.t
program. That's because it would be loaded, it would inherit from your base class, the INIT
block would fire and Rube Goldberg would be laughing in his grave.
Sadly, this leads to nasty bugs like this one I blogged about almost 3 years ago. Basically, if you use $some::module
, the code in that module is executed before INIT fires but the subroutines are not (aside from import()
). However, if you load the code directly with, say perl t/lib/path/to/my/test/class.pm
, the INIT
block fires before the code in the module is executed. You can read perldoc perlmod for more information about INIT
and friends.
How does this impact Moose?
At first glance, you might not think issues with INIT
and friends would impact Moose, but there are a couple of subtle repercussions. For example, we know that subroutine attributes only fire at compile time. That's why the Moose manual explains how to use inherited subroutine attributes with Moose. In short, you have to ensure that you're inheriting at compile-time, not at CHECK/INIT/UNITCHECK or runtime:
BEGIN { extends 'Some::Class' }
Since Moose isn't part of the Perl language and much of its behavior fires at runtime, you can have some strange issues, such as when I wrote the following code:
package TestsFor::Person::Employee;
use Test::Class::Moose extends => 'TestsFor::Person';
sub extra_constructor_args {
return ( employee_number => 666 );
}
BEGIN {
after 'test_constructor' => sub {
my $test = shift;
is $test->test_person->employee_number, 666,
'... and we should get the correct employee number';
};
}
1;
That's really ugly and, in fact, I received an email from someone who called me on that, arguing that Test::Routine and Test::Roo were simpler in this case. I think he was right and I thought for a while about the best way to resolve this. You shouldn't have to think about timing issues like this when you're writing your test classes.
A Cleaner Test::Class::Moose
Today I've released Test::Class::Moose
version 0.11 and it makes this case easier to handle. Your base class now can look like this:
package My::Test::Class;
use Test::Class::Moose;
1;
In fact, you may not even need a base test class at all — just have your classes use Test::Class::Moose
directly — but a base test class is handy enough that people put all sorts of interesting code in there.
So how do you do the INIT
trick that lets you run an individual test class? I now recommend use of a driver *.t
script for your test suite. Here's what one might look like:
use Test::Class::Moose::Load 't/lib';
my $test_suite = Test::Class::Moose->new(
show_timing => 1,
randomize => 0,
statistics => 1,
test_classes => $ENV{TEST_CLASS},
)->runtests;
my $report = $test_suite->test_report;
# the reporting object, of course, can be ignored if you
# don't need reporting on the test suite
Unlike Test::Class
, Test::Class::Moose
has a constructor that lets you control the test suite behavior prior to running the suite. There is a new attribute, test_classes
which takes the name of a test class or an array reference of test class names. Only those test classes will be run. Yes, you could have written a Test::Class::Moose
subclass to control this, but I wanted to make this built-in because running individual classes is a common use case. Now, instead of this:
prove -lv t/lib/path/to/my/test/class.pm
You do this:
TEST_CLASS=Class::I::Want::To::Run prove -lv t/test_class_tests.t
And things work just like the are supposed to. As a bonus, that nasty BEGIN
block in my TestsFor::Person::Employee
class goes away:
package TestsFor::Person::Employee;
use Test::Class::Moose extends => 'TestsFor::Person';
sub extra_constructor_args {
return ( employee_number => 666 );
}
after 'test_constructor' => sub {
my $test = shift;
is $test->test_person->employee_number, 666,
'... and we should get the correct employee number';
};
1;
And your test classes now look like proper Moose code rather than worrying about BEGIN
versus INIT
phases.
I'll be giving a talk about Test::Class::Moose
at YAPC::NA and I've also proposed it for YAPC::EU. I hope to see you there!
Wouldn’t it be easier (and more featureful!) to say “
test_classes => \@ARGV,
” and then do the following?Aristotle: well, damn. You're right. How could I have overlooked the arisdottle?
This actually means a tiny change in the code to handle the case of an empty array reference, but that should be easy.
Then how did the code handle the case of empty
$ENV{'TEST_CLASS'}
?It was undefined, not an empty array ref.