Monday, August 20, 2007

Using Perl's Test::Class for Organized Unit Testing

I'm not going to go over the merits of unit testing. I've heard it all and discussed both sides till blue in the face. If you're unsure about unit testing, then please google it or ask some programmers you know about it.

What I do want to discuss is how one does unit testing in Perl. It's pretty simple, fortunately.

In the world of testing classes in CPAN, there are a few that you'll actually use: Test::Simple, Test::More, Test::Harness, and Test::Class. There are basically two schools of testing that is either Test::Harness based or Test::Class based. Test::Harness school runs a series of scripts which have tests in them top down, usually with a plan at the top of the file (plan is the number of tests you are planning on running). Test::Class manages itself, using Test::Class for tests and to run them (Test::Class allows you to have a plan per Test::Class subclass subroutine... I'll explain in a bit).

Today, I'm not going to talk about Test::Harness or Test::Simple. I will some other time.

I will talk about Test::More and Test::Class. They're pretty awesome and really really simple to use. Don't believe me?! Let's jump into some code.

In this example set, we're going to be dealing with a Hotdog Vendor class. It's purpose is to provide a way to know about a Hotdog Vendor and what's in his cart (east coast style). The Hotdog Vendor class only does a few things: takes the vendors name and how many hotdogs he plans on selling. We're assuming in this example a hotdog is a bun with a frank in it with mustard and kraut (yeah, sounds awesome).

Here's the Hotdog Vendor class (download here):

#
# HotdogVendor.pm: Provide a Hotdog Vendor
#
package HotdogVendor;

use strict;
use warnings;

sub new {
my ($class, $name, $how_many) = @_;
my $attrs = {
franks => $how_many,
buns => $how_many,
mustard => $how_many,
kraut => $how_many,
name => $name
};
bless ($attrs, $class);
}

sub _use_product {
my ($self, $product, $how_many) = @_;

if (($self->{$product} - $how_many) < 1) {
die "[$self->{name}] use_$product: Unable to processor order, not enough $product (you wanted $how_many, I only got $self->{$product}";
}

$self->{$product} -= $how_many;
}

sub _has_product {
my ($self, $product) = @_;

if (!defined $self->{$product}) {
die "[$self->{name}] has_$product: undefined product (wrong vendor?!)";
}

return ($self->{$product});
}

# Franks
sub use_franks {
my ($self, $how_many) = @_;

$self->_use_product('franks', $how_many);
}
sub has_franks {
my ($self) = shift;

return ($self->_has_product('franks'));
}

# Buns
sub use_buns {
my ($self, $how_many) = @_;

$self->_use_product('buns', $how_many);
}
sub has_buns {
my ($self) = shift;

return ($self->_has_product('buns'));
}

# Mustard
sub use_mustard {
my ($self, $how_many) = @_;

$self->_use_product('mustard', $how_many);
}
sub has_mustard {
my ($self) = shift;

return ($self->_has_product('mustard'));
}

# Kraut
sub use_kraut {
my ($self, $how_many) = @_;

$self->_use_product('kraut', $how_many);
}
sub has_kraut {
my ($self) = shift;

return ($self->_has_product('kraut'));
}

# Name
sub name {
my ($self) = shift;

return ($self->{name});
}

1;



As you can see, it saves the name and amount for each product. It then provides subroutines to get the amount of product left, use up some product, and see what the vendor's name is. Pretty simple.. excuse my copy and paste.

Now we need to write some tests to make sure the subroutines work. It's pretty simple, in fact writing tests is brain dead simple it almost seems like it's too simple to even bother with. Wrong. Things change over time, simple changes can cause big problems if not checked. Anyways, enough preaching, time for tests (which should have came before the code, but you knew that already, eh?).

Here is the Hotdog Vendor Test class (download here):

#
# HotdogVendor_Test.pl: HotdogVendor.pm Test (Test::Class, unit testing)
#

package HotdogVendorTest;

use base qw(Test::Class);
use Test::More;

use HotdogVendor;

# Test that name is saved on new vendor creation
sub test_name : Test(1) {
my $name = "Thomas";
my $hotdogVendor = HotdogVendor->new($name, 100);
is ($hotdogVendor->name, $name, 'name saved');
}

# Test product franks is saved and used works
sub test_franks : Test(2) {
my $how_many = 100;
my $hotdogVendor = HotdogVendor->new('Chris', $how_many);
is ($hotdogVendor->has_franks, $how_many, 'franks amount saved');
$hotdogVendor->use_franks(60);
is ($hotdogVendor->has_franks, 40, '100 - 60 franks is 40 franks');
}

# Test product buns is saved and used works
sub test_buns : Test(2) {
my $how_many = 100;
my $hotdogVendor = HotdogVendor->new('Chris', $how_many);
is ($hotdogVendor->has_buns, $how_many, 'buns amount saved');
$hotdogVendor->use_buns(60);
is ($hotdogVendor->has_buns, 40, '100 - 60 buns is 40 buns');
}

# Test product mustard is saved and used works
sub test_mustard : Test(2) {
my $how_many = 100;
my $hotdogVendor = HotdogVendor->new('Chris', $how_many);
is ($hotdogVendor->has_mustard, $how_many, 'mustard amount saved');
$hotdogVendor->use_mustard(60);
is ($hotdogVendor->has_mustard, 40, '100 - 60 mustard is 40 mustard');
}

# Test product mustard is saved and used works
sub test_kraut : Test(2) {
my $how_many = 100;
my $hotdogVendor = HotdogVendor->new('Chris', $how_many);
is ($hotdogVendor->has_kraut, $how_many, 'kraut amount saved');
$hotdogVendor->use_kraut(60);
is ($hotdogVendor->has_kraut, 40, '100 - 60 kraut is 40 kraut');
}
1;


As you can see, it's using the "secret" Perl attribute technique. You can look that up yourself. Just know that it is pretty much what it seems. You can search about the specifics on the Test::Class CPAN page. The one I'm using is 'Test(<number of tests in this subroutine>)', which is pretty simple. A much easier way to organize tests, versus Test::Harness.

How do you run the tests? Here is how (download here):

#!/opt/local/bin/perl
#
use strict;
use warnings;

use Test::Class;

use HotdogVendorTest;


Test::Class->runtests;


The output:

$ /opt/local/bin/perl run_tests.pl
1..9
ok 1 - buns amount saved
ok 2 - 100 - 60 buns is 40 buns
ok 3 - franks amount saved
ok 4 - 100 - 60 franks is 40 franks
ok 5 - kraut amount saved
ok 6 - 100 - 60 kraut is 40 kraut
ok 7 - mustard amount saved
ok 8 - 100 - 60 mustard is 40 mustard
ok 9 - name saved


Pretty simple? Yes. Unit testing is simple and you can be as resource intensive and take as long as you want to run the tests, as that stuff doesn't matter. The only thing that matters is testing tiny parts of your class and program modules. It's pretty easy if your code isn't one big huge function that does 10 more things than it needs to be doing.

Comments? Suggestions? This is my first in a big series of Perl and Mac development articles. I'm starting off simple so I have something to build on for later articles. Hrm, now that I think about it... what do you want to know about??

Let me know and happy testing!

3 comments:

Unknown said...

Nice post - always nice to see folk using T::C :-)

I do think you're a little off however when you say "There are basically two schools of testing that is either Test::Harness based or Test::Class based" since you can use T::C with Test::Harness - indeed it's my usual way of working.

Just rename your run_tests.pl test.pl, or stick t/run_tests.t and Test::Harness will run it fine!

It's one of the nice things about Test::Harness/TAP - the ability to have things as different as T::C, Test::More, Test::Group, etc. all playing nicely together.

Christopher Humphries said...
This comment has been removed by the author.
Christopher Humphries said...

That is true, my comments were a bit harse and maybe too narrow.

I apologize.