Test-Driven Development in Perl: How To Set Up Your Testing Environment For Perl TDD

The Genealogy Webmaster’s Journal is a series of articles advancing the technological State of the Art underlying our online genealogy.

Test-Driven Development has several flavors when practiced in Perl. I was looking for a way to transfer my experience with jUnit, PHPUnit, and CppUTest to my Perl development. I have created a set-up that works for me, and I thought I’d share that with you!

This essay does not contain a lot of explanation. I’m assuming you can follow the links and read the tutorials. If you’re like me, the missing piece is the working setup – how the files are organized for my test environment.

How to Use Test::Class for Perl Unit Testing and Perl TDD

So far, the Perl CPAN module Test::Class seems to fill the bill. Start by reading the documentation:

  • Test::Class Read this page from top to bottom. Take particular note of the section, “Help for confused jUnit users.” See the “Community” section for links on where to discuss and get help.
  • Test::More Test::Class sits on top of Test::More, so be sure to read this documentation and tutorial if you’re not already familiar with how this style of Perl testing works. Note the links to “Related Modules” at the upper right of this page.
  • Test::Tutorial A more complete tutorial for Test::Simple and Test::More.
  • Prove We will use the Perl Application, prove, to run our tests.

Perl Testing Bookmarks Including Perl CGI Testing

I also found the following articles useful:

For this setup, I am running Perl v5.14.2, Binary build 1402 [295342] provided by ActiveState http://www.ActiveState.com Built Oct 7 2011 15:58:41, on my old MacBook running Snow Leopard. I found that this port of Perl does not include the MySQL drivers, which certainly simplifies my development effort. This guarantees I won’t be tempted to convert any PHP/MySQL code back to Perl, because I can’t run it locally!

Therefore this article uses very simple classes. I’m not using mock objects or a database layer. These are merely “proof of concept” files to ensure that I have a working TDD testing environment.

My Perl Testing Configuration Begins With Makefile

For all of my personal projects, I leave myself a Makefile in my top-level folder. Whenever I come back to one of my own projects, I first look in the Makefile, so that I know where to begin! Therefore, we’ll start with the Makefile:

all:
        prove -lv --merge -Dr

The command “prove,” as you will recall from reading the documentation linked above, runs tests through the “Test Anything Protocol” common to Test::Class, Test::More, and most other xUnit testing systems. The command line option -l says to add “lib” to the path for your tests; -v (verbose) says print all test lines; –merge says merge STDOUT with STDERR output; -D (dry run) says to not actually execute the tests; -r (recursive) says to search out all tests in the directory structure. We need to remove the -D option to actually run the tests.

Running the “make” command yields:

$ make
prove -lv --merge -Dr
./t/tests/run.t

Running “prove -lv –merge -r” shows the entire test output (click to expand the output):

$ prove -lv --merge -r
./t/tests/run.t
./t/tests/run.t ..
#
# Test::Person->constructor
ok 1 - Person->can('new')
ok 2 - ... and the constructor should succeed
ok 3 - ... and the object it returns isa Person
#
# Test::Person->first_name
ok 4 - Person->can('first_name')
ok 5 - ... and first_name should start out undefined
#
# Test::Person->full_name
ok 6 - Person->can('full_name')
ok 7 - ... and full_name() should croak() if either name is not set
ok 8 - ... and full_name() should croak() if either name is not set
ok 9 - The name of a person should render correctly
#
# Test::Person->last_name
ok 10 - Person->can('last_name')
ok 11 - ... and last_name should start out undefined
ok 12 - ... and setting its value should succeed
#
# test::Person::Employee->constructor
ok 13 - Person::Employee->can('new')
ok 14 - ... and the constructor should succeed
ok 15 - ... and the object it returns isa Person::Employee
#
# test::Person::Employee->employee_number
ok 16 - Person::Employee->can('employee_number')
ok 17 - ... and employee_number should not start out defined
ok 18 - ... but we should be able to set its value
#
# test::Person::Employee->first_name
ok 19 - Person::Employee->can('first_name')
ok 20 - ... and first_name should start out undefined
#
# test::Person::Employee->full_name
ok 21 - Person::Employee->can('full_name')
ok 22 - ... and full_name() should croak() if either name is not set
ok 23 - ... and full_name() should croak() if either name is not set
ok 24 - The employee name should render correctly
#
# test::Person::Employee->last_name
ok 25 - Person::Employee->can('last_name')
ok 26 - ... and last_name should start out undefined
ok 27 - ... and setting its value should succeed
1..27
ok
All tests successful.
Files=1, Tests=27,  3 wallclock secs ( 0.05 usr  0.02 sys +  0.51 cusr  0.08 csys =  0.66 CPU)
Result: PASS

Running without -v produces more compact output (using command line substitution to change -lv to -l):

$ ^lv^l
prove -l --merge -r
./t/tests/run.t .. ok
All tests successful.
Files=1, Tests=27,  0 wallclock secs ( 0.03 usr  0.01 sys +  0.43 cusr  0.04 csys =  0.51 CPU)
Result: PASS

This is the same result, 27 tests pass, without the individual OK lines.

My Perl Test Runner

Here is the test runner, t/tests/run.t:

#!/usr/bin/env perl -T

use lib 'lib';

use Test::Class::Load qw<t/tests>;
Test::Class->runtests;

Note that I use the “env” command to invoke Perl. That’s because Snow Leopard has ancient perl v5.10.0 installed as /usr/bin/perl, but I have my environment PATH variable to point to my newer ActiveState perl. The “Running Tests” section of the Test::Class documentation explains how to load up all your tests using Test::Class::Load, and then run them with the runtests method.

My Perl Test File Locations

Where do I put my tests? At my top level folder (where my Makefile resides), I have the folder t for tests. It looks like this:

t/
t/tests/
t/tests/run.t
t/tests/Test/
t/tests/Test/Person.pm
t/tests/Test/Person/
t/tests/Test/Person/Employee.pm

In the above structure, run.t is the test runner we see above. Person.pm is a test harness subclass which tests my Person.pm module. Employee.pm is a subclass of that Person.pm. Test/Person/Employee.pm tests the Employee.pm module, which is a subclass of the Person.pm module.

The test-class structure mirrors the class structure in my production code folder, lib:

lib/
lib/Person.pm
lib/Person/
lib/Person/Employee.pm

My Perl Test File Contents

Finally, here is the code for my test classes and production classes being tested. t/tests/Test/Person.pm:

package Test::Person;

use Test::Most;
use base 'Test::Class';

sub class { 'Person' }

sub startup : Tests(startup) {
my $test = shift;
my $class = $test->class;
eval "use $class";
die $@ if $@;
}

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

sub person {
return shift->{person};
}

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 {
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', '... and setting its value should succeed';
}

sub last_name : Tests {
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(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 ) = @_;
$person              = $test->class->new unless defined $person;
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 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 either name is not set';

}

1;

Here is t/tests/Test/Person/Employee.pm:

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

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

1;

Here is the production code lib/Person.pm:

package Person;

use Moose;

has first_name => ( is => 'rw', isa => 'Str' );
has last_name  => ( is => 'rw', isa => 'Str' );

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->first_name . ' ' . $self->last_name;
}

1;

Here is the production code lib/Person/Employee.pm:

package Person::Employee;

use Moose;
extends 'Person';

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

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

1;

How To Colorize Perl Code in WordPress

To create this post, I installed the WordPress plugin “SyntaxHighlighter Evolved.” I like it! This is a short section. It’s that simple!