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.

6 Comments

Does this do any more than SKIP_CLASS?

@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?

Would it be possible/worth it to simplify the interface further to a :Concrete attribute, so you could just write

sub duration : Tests(2) Concrete {
    my $test     = shift;
    my $instance = $test->get_instance;

    can_ok $instance, 'get_duration';        
    my $duration = $instance->get_duration;
    isa_ok $duration, 'DateTime::Duration';
}

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.)

About Ovid

user-pic Freelance Perl/Testing/Agile consultant and trainer. See http://www.allaroundtheworld.fr/ for our services. If you have a problem with Perl, we will solve it for you. And don't forget to buy my book! http://www.amazon.com/Beginning-Perl-Curtis-Poe/dp/1118013840/