Reusing Test Code with Test::Class

| 1 Comment

After reading Organizing Test Suites with Test::Class, you're probably and saying "that's a heck of a lot of work just for testing a class." If this were all there is to it, you'd be perfectly justified in forgetting about Test::Class. However, Test::Class really shines when it comes to code re-use. Consider writing a subclass of Person named Person::Employee. I'll keep it simple by only providing an employee_number method, but you'll quickly understand the benefits.

     package Person::Employee;

     use Moose;
     extends 'Person';

     has employee_number => ( is => 'rw', isa => 'Int' );

     1;

Here's its test class:

     package Test::Person::Employee;

     use Test::Most;
     use base 'Test::Person';

     sub class {'Person::Employee'}

     sub employee_number : Tests(3) {
         my $test     = shift;
         my $employee = $test->class->new;

         can_ok $employee, 'employee_number';
         ok !defined $employee->employee_number,
             '... and employee_number should not start out defined';

         $employee->employee_number(4);
         is $employee->employee_number, 4,
             '... but we should be able to set its value';
     }

     1;

Notice that instead of inheriting from Test::Class, the test inherits from Test::Person, just like Person::Employee class inherited from Person. Also, this overrides the class method to ensure that tests know which class they're using.

Remember to add Test::Person::Employee to t/run.t:

     #!/usr/bin/env perl -T

     use lib 't/tests';

     use Test::Person;
     use Test::Person::Employee;

     Test::Class->runtests;

    And when we run it t/run.t:

     All tests successful.
     Files=1, Tests=31,  1 wallclock secs ( 0.25 cusr +  0.06 csys =  0.31 CPU)

Whoa! Wait a minute. This new test class only had three tests. The previous run ran with 14, so how come the report says it ran 31?

Test::Person::Employee inherited the tests from Test::Person. The 14 original tests plus the 14 inherited tests and the 3 added tests add up to 31 tests! These aren't frivolous tests, either. Look at the new test's output:

     # Test::Person::Employee->constructor
     ok 16 - Person::Employee->can('new')
     ok 17 - ... and the constructor should succeed
     ok 18 - ... and the object it returns isa Person::Employee
     #
     # Test::Person::Employee->employee_number
     ok 19 - Person::Employee->can('employee_number')
     ok 20 - ... and employee_number should not start out defined
     ok 21 - ... but we should be able to set its value
     #
     # Test::Person::Employee->first_name
     ok 22 - Person::Employee->can('first_name')
     ok 23 - ... and first_name should start out undefined
     ok 24 - ... and setting its value should succeed
     #
     # Test::Person::Employee->full_name
     ok 25 - Person::Employee->can('full_name')
     ok 26 - ... and full_name() should croak() if the either name is not set
     ok 27 - ... and full_name() should croak() if the either name is not set
     ok 28 - ... and setting its value should succeed
     #
     # Test::Person::Employee->last_name
     ok 29 - Person::Employee->can('last_name')
     ok 30 - ... and last_name should start out undefined
     ok 31 - ... and setting its value should succeed

By not explicitly hard-coding the class name in the tests and because Test::Person::Employee had overridden the class method, these new tests run against instances of Person::Employee, not Person. This demonstrates that subclassing did not break any of the inherited behavior! However, if you do need to alter the behavior of one of those methods, as you might expect with object-oriented code, all you need to do is override the corresponding test method. For example, what if employees must have their full names listed in the format "last name, first name"?

     sub full_name {
         my $self = shift;

         unless ( $self->first_name && $self->last_name ) {
             Carp::croak("Both first and last names must be set");
         }

         return $self->last_name . ', ' . $self->first_name;
     }

The appropriate test method in Test::Person::Employee might look like:

     sub full_name : Tests(no_plan) {
         my $test   = shift;
         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';

         $person->last_name('Public');
         is $person->full_name, 'Public, John',
           '... and setting its value should succeed';
     }

Make those changes and all tests will pass. Test::Person::Employee will call its own full_name test method and not that of its parent class.

Refactoring test classes

There's a lot of duplication in the full_name test which you should factor out into common code. The well-known (if poorly-practiced) aphorism that test code is just code is even more true when Test::Class. Well-factored tests are easier to understand, to maintain, and to modify than poorly-factored tests.

Refactoring with methods

One approach to reduce duplication in Test::Person class might be to create helper methods:

     sub full_name : Tests(no_plan)
         my $test   = shift;
         $test->_full_name_validation;

         my $person = $test->class->new(
             first_name => 'John',
             last_name  => 'Public',
         );

         is $person->full_name, 'John Public',
           'The name of a person should render correctly';
     }

     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';
     }

And in Test::Person::Employee:

     sub full_name : Tests(no_plan)
         my $test   = shift;
         $test->_full_name_validation;
         my $person = $test->class->new(
             first_name => 'Mary',
             last_name  => 'Jones',
         );
         is $person->full_name, 'Jones, Mary',
           'The employee name should render correctly';
     }

Just like with any other OO code, subclasses inherit and can override the _full_name_validation method.

Refactoring with fixtures

When writing test classes, the startup and shutdown methods are very handy, but those run only at the beginning and end of your test class. Sometimes you need code to run before the beginning and end of every test method. In the Person examples, many of the test methods contained this line:

     my $person = $test->class->new;

You really may not want to duplicate that every time, so you can use what's known as a fixture. A fixture is "fixed state" for your tests to run against. These allow you to remove duplicate setup code from your tests and to have a controlled environment. You might write:

     sub setup : Tests(setup) {
         my $test        = shift;
         my $class       = $test->class;
         $test->{person} = $class->new;
     }

If you want to start with a known set of data, you could write:

     sub setup : Tests(setup) {
         my $test        = shift;
         my $class       = $test->class;

         $test->{person} = $class->new(
             first_name => 'John',
             last_name  => 'Public',
         );
     }

Now all of your test methods can simply use $test->{person} (you can even make that a method if you prefer) to access a new instance of the class you're testing without having to duplicate that code.

The corresponding teardown method is useful if you need to clean up on a per test basis. This can be useful if you run tests against a database.

Next time, I'll discuss how to manage test classes with Test::Class.

1 Comment

Ovid,

Thanks for these two articles. I've just started learning about TDD and they are just what I needed to grok the situation so I can start writing tests effectively and quickly.

--[Lance]

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

sponsored by the How to Make a Smoothie guide

Categories

Pages

About this Entry

This page contains a single entry by Ovid published on March 10, 2009 1:50 PM.

Organizing Test Suites with Test::Class was the previous entry in this blog.

Making Your Testing Life Easier is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Powered by the Perl programming language

what is programming?