Writing Tests in Perl
August 27th 2008
Lately I've been writing a lot of unit tests for a big Perl application. I'm going to show you how to use the Test::More module. For really complex test suites you'll want to turn to TAP::Harness, but for now we'll pretend it doesn't exist.
But First A Detour Into Class Design
It's important I share a little trick with you, because it will show up later in our testing. In fact, I've already written about this before. This time I'm just putting it in the context of designing a class's interface.
Yesterday I'm sitting there writing a User class.
Called User. Go figure. Like most classes in Perl it
uses a hash to store the data members. In the case of this class we
have items like e-mail address, password, date of birth, and so on.
I first wrote some methods for accessing and modifying these
values.
sub email {
my $self = shift;
# If we received another argument then we treat the method
# as a setter, and modify the email address.
if (@_) {
$self->{email} = shift;
}
# Whether we changed the value of or not we return the address.
# So called with no arguments this method is a plain getter.
return $self->{email};
}
Pretty simple stuff. Then I started writing the other methods.
sub password {
my $self = shift;
if (@_) {
$self->{password} = shift;
}
return $self->{password};
}
sub dob {
my $self = shift;
if (@_) {
$self->{dob} = shift;
}
return $self->{dob};
}
Uh-oh, those are starting to look pretty similar…. I don't like writing the same code over and over. You shouldn't like it either. Whenever you find yourself copying code that should trigger alarm bells in your head. Thankfully Perl lets us write the above function once and define it for a number of data members, leading right into the trick I mentioned.
First we need a list of all the data members our class will have
getters and setters for. This is what I used for
my User class:
our @class_members = qw(
id
first_name
last_name
email
address
password
phone
fax
dob
gender
level
);
The reason this is declared our and
not my will be explained once we get into writing
tests. For now let's just say we want the outside world to be able
to get a list of our getter and setter methods. Now what we do is
take this list and loop over it, defining a function just we did
above.
for my $getter_setter (@class_members) {
# There may be some members that require special methods to handle them.
# If that's the case, we can skip over them here. You may think to just
# remove them from @class_members, but I recommend you don't. It will help
# the test cases later to leave it in.
next if $getter_setter eq 'address';
# Without this Perl is going to complain about what we're about to do.
no strict 'refs';
# I want $user->first_name() to return $self->{FIRST_NAME}. So I make the
# member name an upper-case version of the method name.
my $member = uc $getter_setter;
# When $getter_setter is 'email' this is the same as writing 'sub email {...'.
# When it's 'password' it is "sub password { ..." and so on.
*$getter_setter = sub {
my $self = shift;
if (@_) {
$self->{$member} = shift;
}
return $self->{$member};
}
}
So now for every member listed in @class_members we
have a method with the same name as that data member. When called
with no arguments it returns the current value, or you can give it
an argument to change the value of the member. All except
for address since we chose to skip that one.
This is a useful technique for compressing a group of similar functions. Especially in the situation where you have lots of boilerplate getter and setter methods. Now let's get into actually writing tests.
Simple Testing
Perl provides a module called Test::Simple that you can use to put together basic test scripts. This post is leading towards a discussion of Test::More, but the nice thing about Test::Simple is that you can use it, and when you're ready you can swap it out for Test::More without having to change anything else. As the documentation says, Test::More is a drop-in replacement for Test::Simple. So if you begin with Test::Simple you're not tied to it forever.
I can use Test::Simple for my class like so:
use Test::Simple tests => 2;
use User;
my $user = new User;
ok($user, 'User::new returns something');
$user->first_name('Eric');
ok($user->first_name(), 'User::first_name works');
When we use Test::Simple we have to provide the number
of tests in our script. In this case two, but two what?
Well we are counting the calls to ok(). It's the one
function Test::Simple provides for actually testing things. You
call ok() with two arguments:
- An expression.
- A short message describing what you're testing.
Then ok() will check the expression. If it evaluates
to something true then the test passes. Otherwise it fails and
you'll see that in the output.
The requirement of counting your tests might seem stupid. You can get around it, but I'm not going to show you how, because I think it's a good idea. Counting tests is not always that easy anyways, and sometimes we have to resort to fancier tools for arriving at that final number. If you're really lazy you can put any number you want and then check the output of your test script; the Test modules will tell you how many tests you really have, at which point you can update the script accordingly.
More importantly, notice that we're not testing everything we
could. We don't test to see if use User works. We
don't ensure that $user actually is an instance
of User. We only check that first_name()
returns something, but it might not be the right data.
Time For Test::More
Let's expand the tests from above and then we'll go over what does what, although I think it will be obvious.
use Test::More tests => 3;
BEGIN {
use_ok('User');
}
my $user = new User;
isa_ok($user, 'User');
$user->first_name('Eric');
is($user->first_name(), 'Eric', 'User::first_name works');
Notice we eliminated the use User and replaced it
with use_ok('User'), wrapped in a BEGIN
block so that it's guaranteed to process before anything else.
The use_ok() function tries to use our
module and considers it a test. If the use fails for
any reason Test::More will tell us. That's test one.
Test two involves isa_ok(). It takes a variable and a
class name and tests whether or not that variable is an instance of
that class. Unlike our Test::Simple script this lets us determine
if the constructor is returning a blessed reference to the correct
class.
Our final test is a modified version of the test we wrote
for first_name(). Except this time we explicitly check
to see if the return value is() what we set it to be.
Negative tests can be written using isnt().
We can be lenient in our value testing by using like()
and unlike(), which test values against regular
expressions.
$user->phone('800-555-1234');
# This test will pass.
like($user->phone, qr/\d{3}\-\d{3}\-\d{4}/, 'phone number looks valid');
Or we can use cmp_ok() to dictate the comparison
operator to be used.
cmp_ok($user->id(), '>=', 2000, 'got large id?');
I don't know why you would test such a thing though. But you can
see the applications. Furthermore, cmp_ok() is useful
for testing the equality of numbers since is() and kin
use the eq operator and not the ==
operator.
Noting Tests Which Should Fail
Tests inside a TODO block are intended to fail, presumably because you haven't implemented everything necessary for them to pass. When you have tests inside a TODO Test::More will take special notice of anything which actually manages to pass. This is really helpful for pointing out to yourself when you've finally written enough code.
TODO: {
local $TODO = 'UserManager class is unfinished.';
my $manager = new UserManager;
isa_ok($manager, 'UserManager');
ok($manager->update_user($user), 'update a user');
ok($manager->remove_user($user), 'remove a user');
}
Once these tests start passing I'd know to move them out of the TODO block and alongside the other tests, at which point failure is not what we expect.
Getting Back to Class Members
Remember I said we were making @class_members visible
for testing? This is why.
Test::More provides a function called can_ok(). It
take two arguments:
- An object.
- Either a single method name, or an array of method names.
Then can_ok() passes if that object has those
methods. Or it fails. It helps us test the existence of our
classes APIs. Since it takes an array we can write this:
my $user = new User; can_ok($user, @User::class_members);
Since we used @class_members to build our getter and
setter methods we know that array names all those methods.
Add that to the fact that can_ok() accepts an array of
method names to test and we have quick means for testing the
existence of all those methods. If we ever adjust the values
of @class_members we don't have to worry about changing
anything in the test.
Further Reading
I suggest you check out the documentation for Test::More, as it's not all that long, and there were things I omitted. Hopefully I've shown you the basics of putting together tests in Perl. If that's true, make sure you write them. Co-workers will appreciate it. ::smile::
Post A Comment
URLs beginning with http:// will be turned into links. By prefixing a paragraph with >> you can turn it into a quote. Check the log for new comments.