Skipping Test::Class tests in abstract base classes
With the latest release of Test::Class::Most, I added the is_abstract feature. With this, you can declare a test class like this:
package TestsFor::TV::Episode::Broadcast;
use Test::Class::Most
parent => 'TestsFor::TV::Episode',
is_abstract => 1;
is_abstract is a non-inherited property of a test class which says "I'm abstract" (no surprise there) and you can check it with:
Test::Class::Most->is_abstract($some_test_class);
The reason for that is simple. Imagine you have a TV::Episode class, but it's an abstract base class which should never be instantiated. You actually have a TV::Episode::Broadcast and TV::Episode::OnDemand classes which are the concrete implementations. You can make tests work in your test classes very cleanly with this.
In your test class TestsFor::TV::Episode, you may have the following test method:
sub duration : Tests(2) {
my $test = shift;
my $instance = $test->get_instance;
can_ok $instance, 'get_duration';
my $duration = $instance->get_duration;
isa_ok $duration, 'DateTime::Duration';
}
Because you've put that in an abstract base test class, your TestsFor::TV::Episode::OnDemand and TestsFor::TV::Episode::Broadcast classes will both attempt to run the duration tests (assuming they're not overridden). This is good because because those classes should present more or less the same core interface, perhaps with extra methods for each class.
But what happens in the base class? It's abstract and shouldn't run that because you won't have an instance. Make sure you have is_abstract => 1 in the import list for Test::Class::Most and rewrite the duration tests as follows:
sub duration : Tests(2) {
my $test = shift;
if ( Test::Class::Most->is_abstract($test) ) {
return "Skipping duration() tests in abstract base class";
}
my $instance = $test->get_instance;
can_ok $instance, 'get_duration';
my $duration = $instance->get_duration;
isa_ok $duration, 'DateTime::Duration';
}
In fact, I'm using this enough that I have the following in my base class:
sub is_abstract {
my $test = shift;
return Test::Class::Most->is_abstract($test);
}
And the above check becomes:
if ( $test->is_abstract ) {
return "Skipping duration() tests in abstract base class";
}
Hopefully this will make your test classes easier to manage.
If you're unfamiliar with Test::Class, see my tutorial on Test::Class. The code shown in this post requires my Test::Class::Most module.
Does this do any more than SKIP_CLASS?
@Rob: yes, you have control. SKIP_CLASS is fine, but it's blunt. is_abstract is self-documenting and you have fine-grained control.
@Ovid: Ah, I think I see now; is it that is_abstract allows you to run most methods in an abstract test class, but you can skip some test methods that require a concrete implementation in a test subclass?
@Rob: exactly. You may not need and SKIP_CLASS may be fine, but if you need control, is_abstract is what you really want.
Would it be possible/worth it to simplify the interface further to a :Concrete attribute, so you could just write
and the check-and-skip would happen for you? (It would obviously be worth leaving the ->is_abstract method as well, for more complex situations.)
@Ben: I once created (but never released) a an extension to Test::Class which would provide attribute "tags" which would let people take actions (such as run a subset of tests) based on tags, but given that Adrian and others have worked on something similar, I abandoned the effort. If anyone wants to extend the attribute mechanism for Test::Class, I don't want to have a potential conflict.
I do, however, like the idea. Of course, I think I'd prefer "abstract" instead of "concrete" because the latter is the default for most Test::Class methods.