Automating Testing with Test::More
Leif Eriksen
OK, so you’ve managed to write your first Perl module, and you’ve managed to install it so you can write ‘use YourModule;
‘ in your scripts. Now you want to provide it to your friends or collegues, but you also want to make sure its pretty well tested before that.
How can you test the functions and methods in your new module ?
One thing I always tell myself, when wondering how to do something in Perl, is ‘You are not the first.’
What this means is that whatever the issue I have come across and am struggling with, someone else has almost certainly struggled, and solved, it before me. And it seems to be in the nature of Perl programmers to provide that solution to others, either through an addition to Perl itself, a CPAN module, a newsgroup posting or on a community website such as this.
And when it comes to testing, Perl has extremely strong support for testing scripts and modules, through the builtin Test::More
and ExtUtils::MakeMaker
facilities.
Test files
To use these facilities you write files containing test cases, in a particular way, and place those files in a particular location.
Lets say you developed a module Monger.pm
, that provides functions and utilites to emulate a Perl programmer (see Perl Mongers). You have developed this module on a *nix machine, in a directory ~/workspace/www
. Lets make this directory our working directory. Hence Monger.pm
has the path of ~/workspace/www/Monger.pm
.
Here is a sample Perl module called Monger.pm
that you can use as you work
your way through this tutorial:
Locating the test files
Lets start to get our testing organised. I’ll show you the conventions Perl expects in testing. By following these conventions, you get to leverage the powerful builtin support provided with Perl.
In the working directory, we make a directory called ‘t
‘ i.e. ~/workspace/www/t
.
You will place your test cases in a file in this t directory, called Monger.t
. The prefix of the filename (the ‘Monger’ part) isn’t important, but the path (the t/
path) and the suffix (the ‘.t
‘ part) are important. By convention, Perl expects test cases to be in ‘dot-T’ files in a directory called ‘t
‘. e.g. ~/workspace/www/t/Monger.t
By following this convention, you get to leverage some useful automations provided by Test::More
and ExtUtils::MakeMaker
Test::More
Test::More
is the backbone of writing test cases for your modules.
In the t/Monger.t
file we will use the facilities provided by Test::More
to test the functions and methods of Monger.pm
.
A ‘dot-T’ file is effectively a normal Perl script, just the suffix is different. So t/Monger.t
starts like most other Perl scripts
Test::More
normally expects to be told how many tests are in the ‘dot-T’ file in question. But as we are actively developing our test cases, we’ll use the ‘no_plan’ directive let it know there is no count available.
So on to writing an actual test case.
Test::More
provides a number of functions to test your modules public interface., the general form of which is method(<param1>, <param2>, “comment”).
That is, they take 3 parameters, 2 compulsory and one optional. They can be loosely described as
You put a call to one of your modules functions or methods in the <actual value> position, the value you expect that function to return in the next position, and a nice description of what the test is supposed to do in the third position if you want.
For example
…
The factorial
function is called with the parameter 0
. We expect that call to return 0
. Test::More
‘s is()
compares the two values for equality, and returns true or false. False comparisons generate a noisy message.
The power of Test::More
is that it keeps a record of what passed and failed, and gives this information to you in a report after running your test cases.
So lets show what a simple t/Monger.t
file could look like :-
So lets break this down into the individual tests.
This tests that we can successfully do a ‘use Monger;’. It actually does do the same as a use
, but just returns a simple OK/NOK status. We have to wrap it in a BEGIN {}
block so that the test occurs at compile time, because this is when the ‘use …’ statements in a script get executed.
Here we check that the package/module Monger (or one of its parent modules, via the ‘@ISA …’ or ‘use base …’ mechanisms) can find a method called ‘new
‘. Maybe you inherited the method from another module, and you want to check that the inheritance is correct. Also, say one day the author of the parent module changes the method name from ‘new
‘ to ‘create
‘ – this test catches that change immediately (or rather immediately you run your test cases again). Note that methods whose implementation is provided by an AUTOLOAD()
function can’t be found via a can_ok
..
OK, so the preceding few lines of code returned an instance of the Monger class – or did it ? This test checks the constructor works as expected, with the attributes supplied.
The foreach loop tests the ‘accessor’ methods of the class, calling GetHeight()
, SetHeight
etc, and checking the retun value via the is()
call. In this case, the values returned are those in the hash passed to the constructor.
This is very similiar to the preceding ‘can_ok’ call, but the first parameter is an instance variable, rather than a class/package name. So the test is ‘can this specific instance of Monger call a method program()
?’
The Monger::program()
method is expected to return undef. Test::More
knows how to compare undef’s for equality.
There are many other useful test methods in Test::More
, check the perldoc. Note that some methods have a negative flavour too – is/isnt, like/unlike etc.
Now because ‘dot-T’ files are just normal Perl scripts, you can run them as such –
If we actually do this, we’ll get a report from Test::More
like this :-
Which indicates a successful test run. Lets break the code, by having the Set<attribute>() methods in Monger.pm
prefix the return value with “ERR”. To
do this, change line 21 in Monger.pm
from:
to:
The resulting test output is :-
Looking at the line “not ok 5 – SetHeight”, the “SetHeight” word comes from the comment parameter to is()
.
You also see the actual and expected values that disagreed in the test.
Now this is all well and good, and we can use this to test all the modules we write from now on, but lets say you wrote dozens of modules, and you also wrote dozens fo ‘dot-T’ files to test them all. Running them all individually via ‘perl t/<file
>.t’ can get pretty boring and labourious. Wouldn’t it be good if we could come up with a way to do this automatically?
ExtUtils::MakeMaker
Again remembering our mantra ‘You are not the first’, Perl already provides just this mechanism through the facilities of the module ExtUtils::MakeMaker
. This module is one of the oldest in Perl, and it shows its age in its slightly clunky interface, but for writing and running test files, very little work is required.
If we write a file called ~/workspace/www/Makefile.PL
that contains just these two lines:
then automated running of test file (and LOTS more besides) is available.
What WriteMakefile does is … write a makefile. In order to get Makefile.PL
to generate this makefile, we run it like a normal Perl script, (you should be
in the www
directory when you do this, and you must have a copy of Monger.pm
in the same
directory for this to work):-
If we examine ~/workspace/www
, we now see we have a new file ‘Makefile
‘, generated by ExtUtils::MakeMaker::WriteMakefile()
.
A makefile is a series of instructions about how to construct and run components of a software system. In Perl’s case, it provides a number of ‘targets’ we can run. The target we are most interested in is the ‘test’ target. To run that,
first undo the “ERR” change we made at line 21 of Monger.pm
so that once
again it looks like:
and then we type:
In this case we get the following output :-
Lets break that down…
The make file issues a command to copy our module to a ‘staging area’. If you had a number of modules, they would be copied here. The modules in the staging area are the ones that get compiled, run etc.
This complex line give a number of instructions to the Perl executable. Firstly it sets an environmental variable PERL_DL_NONLAZY=1, (you dont have to care what it does, so skip to the end of this sentence if you want) which forces dynamic libraries loaded by Perl to resolve their symbols now, rather than trust them to resolve at runtime. If you dont understand what that means, that’s OK. Next is the path and name of the Perl executible to run. This is passed the remaining text as parameters to Perl. The first tells Perl to load the module “ExtUtils::Command::MM
“. The “-e” parameter tells Perl that the next parameter is a Perl script to run. The parameters after the “-e” tell Perl to run the test_harness()
function (from the ExtUtils::Command::MM
package), with the parameters (, ‘
blib/lib
‘, ‘blib/arch
‘) (the verbose flag and the paths to the staging areas), on all the *.t files in the t directory. Simple!!
The result is that if we have lots of modules and test cases, they all get run – here’s an example of an extensive test run of some other software not shown here:
You can see that we had ‘make test
‘ pass 4 different directories for test_harness()
to run against. That is controlled by options in the Makefile.PL
file. Read the ExtUtils::MakeMaker
perldoc to see how that is done.
ExtUtils::MakeMaker
generates a makefile with a whole slew of useful targets – install, dist, clean – again see the perldoc.
Conclusion
So there you go, a fairly comprehesive introduction to getting a test harness up and running in Perl. Hopefully it will help you write solid, robust, reliable Perl modules.
See also
Leif Eriksen