Unit Testing is a Little Bit Like Flossing
You know you should floss, but you don’t have time to do it right now. Sure, you floss when you have an annoyance, like that piece of pulled pork caught between your molars. And you floss for a couple of days, after your dental hygienist has given you ‘the lecture’. But in the end, you don’t have time to do it right now. Maybe later.
Writing unit tests is a little bit like flossing. You know it’s a good thing to do. It may not feel like fun while you’re doing it, but you’re almost always glad you did it when you’re done. Like flossing, if you’re going to do it, and do it consistently, you need to try and establish a habit. And there’s no time like now to get started.
In the directory where you stash your modules, create a subdirectory named “t“. Yes, you’ve seen this directory name before when installing a module from CPAN. Don’t panic! You’re not going to have to build a complete distribution. You’re just creating a place to stash your tests that is both convenient and follows the conventions used by the Perl prove utility. Now, the next time you create some new function or method in one of your modules, create a unit test for that function in your “t” directory. For example, let’s say I’m creating a new utility function call mySum() in the module MyModule. I’ll also create a new unit test script named mySum.t. As I write the function in the module, I’ll also start writing the unit tests in the test script. Writing some logic to handle an edge case? Write a test to probe that edge. Test it. Right now! Now code up the next hard part, and test that. Here’s a simple example you can use as a starting template:
#!/usr/bin/env perl
#
# File: t/mySum.t
# Usage: prove mySum.t
# Abstract: Test my wonderful MyMod::mySum() service.
use warnings FATAL => qw(all); # Make all warnings fatal.
use strict; # Keep things squeaky clean.
use Test::More; # Lots of testing goodies.
use File::Basename; # Easy basic filespec parsing.
use lib '..'; # Where to find our own modules.
my $ownName = basename($0); # For error reporting.
die("? $ownName: no command line args expected\n_ ", join(' ', @ARGV), "\n_")
if (scalar(@ARGV) > 0);
use_ok('MyMod') or exit(1); # The module we're testing.
is( MyMod::mySum(), 0, "sum of nothing is zero" );
is( MyMod::mySum(undef(), undef()), 0, "sum of two nothings is zero" );
is( MyMod::mySum(1,2,3,'fred'), undef(), "fred is not a number" );
is( MyMod::mySum(2,2), 4, "two small positive integers" );
my $x = 2;
is( MyMod::mySum($x,$x), 4, "two small positive integers" );
is( MyMod::mySum($x,\$x), undef(), "can't sum a scalar reference" );
# etc.
done_testing();
# EOF: mySum.t
The example above uses the is() test function which, along with ok(), covers a lot of the sort of simple test cases you’ll want to do. But there are lots of other very useful test functions beyond these, so be sure to check out the Test::More documentation for more details.
To run your test, from your module directory, you can do either:
$ t/mySum.t
ok 1 – use MyMod;
ok 2 – sum of nothing is zero
ok 3 – sum of two nothings is zero
ok 4 – fred is not a number
ok 5 – two small positive integers
ok 6 – two small positive integers
ok 7 – can’t sum a scalar reference
1..7
$
…or….
$ prove t/mySum.t
t/mySum.t .. ok
All tests successful.
Files=1, Tests=7, 0 wallclock secs ( 0.02 usr 0.00 sys + 0.02 cusr 0.00 CPU)
Result: PASS
$
As your test library grows, you can just enter the prove command with no parameters and it will automatically run all of the *.t files it finds in your test directory.
So, don’t worry about back-filling all of those unit tests you should have written. But do start getting into the habit of creating unit tests, function by function, as you create new code, particularly for those utility functions that have very narrow, well defined jobs. For your convenience, here is the toy module that was used with the above unit test.
package MyMod;
# Simple demo module with simple demo function.
use warnings FATAL => qw(all);
use strict;
use Scalar::Util qw(looks_like_number);
sub mySum
# Precondition: Parameters consist of zero or more numeric values.
#
# Postcondition: The sum of all the numeric values is returned as our
# functional value. Undefined parameters are silently
# ignored. We return undefined if one or more values
# do not appear to be numeric (i.e. a usage error). We
# return 0 if there are no defined parameters.
#
# Unit Test: t/mySum.t
{
my $sum = 0;
foreach my $x (@_)
{
next if (not defined($x));
return() if (not looks_like_number($x));
$sum += $x;
}
return($sum);
}
1; # So endith your typical Perl module.