Making Your Testing Life Easier

| 2 Comments

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.

2 Comments

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.

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 12, 2009 2:00 PM.

Reusing Test Code with Test::Class was the previous entry in this blog.

Using Test Control Methods with Test::Class 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?