|Perl: the Markov chain saw|
Basic Testing Tutorialby hippo (Bishop)
|on Sep 11, 2019 at 10:32 UTC||Need Help??|
One of the widely-acknowledged strengths of Perl is its use of testing as a cornerstone. Almost every module on CPAN comes with its own test suite and there are many modules available to help with testing and to make it as painless as possible. For those looking to add testing to an existing code base or looking to get started with TDD here is a guide to the basics.
Test scripts and TAP
Tests in perl are, at their heart, simple scripts which evaluate a series of boolean expressions and output the results. No doubt you could write such a script in a few minutes and the output would tell you how your test criteria have fared. However, there is a standard form to the output of the test scripts which is best adhered to. This is the Test Anything Protocol or TAP for short. By having your script output the results in this format, they can be analysed by a wealth of other programs called TAP Harnesses to provide summary data, highlight failures, etc.
TAP is an open protocol but was written for Perl originally. It is intentionally very simple. The first line of output is a range of test numbers starting at 1 and each subsequent line consists of three fields: a pass/fail flag which is either "ok" or "not ok", the number of the current test and an optional description of that test. Thus a script containing a single, passing test might output this when run:
A first test script
So, let's write a simple test script which will output TAP. Suppose we want to check that nobody is running our code far in the past. Here would be a trivial test.
When we run this without time-travelling we see this output:
and we see that our test has passed.
Of course there are modules on CPAN to help with testing. The simplest of these is, appropriately enough, Test::Simple which is a core module. It is little more than a wrapper with 2 handy functions to ensure that your TAP output is in the correct format. We can rewrite our simple century test with this module:
Now there are no print statements because the module takes care of all the output. The tests => 1 on line 4 sets the number of tests we expect to run, so we no longer need to print "1..1\n" ourselves. Similarly, the ok function evaluates the first argument as a boolean expresssion and outputs the correct TAP line as a result. The second argument is the optional description.
Technically it is optional but I would encourage you very strongly to include a description for any test. If you have a script with say 50 tests in it and test 37 fails but has no description, how will you know what is wrong? Make life easy for yourself (and your collaborators and even the users) by describing each test in the TAP output.
Other testing functions
While the ok function is useful, the output is a simple pass/fail - it doesn't say how it failed. If our century test fails we don't know what year it thinks it is. For that we would need to write more code or use code someone else has written. Fortunately there is a plethora of other testing modules to choose from, the most common of which is Test::More (also in core). This gives us a heap of other functions so that we can easily perform different types of evaluations and receive better feedback when they fail.
Let's use Test::More and its handy cmp_ok function in our script.
Note that I've introduced a bug here (using _year instead of year) so that the test will likely fail. Now our test output looks like this:
We can see at a glance what is being tested and that the year we actually have (119) is clearly wrong so we need to fix the bug. All lines in TAP which start with a hash (#) are comments for the reader: Test::More and friends use this to give us verbose reports about how things have gone wrong.
There are a number of other useful comparator functions in Test::More such as is for simple equality, like for regex and so on. These are fully explained in the Test::More documentation, but their usage is quite straightforward. Let's add a couple of other tests to see how they are used.
There are also control flow structures such as skip to avoid running tests in certain circumstances such as an invalid underlying O/S or absence of a particular module. We could use this here to skip the test of the dayname if a non-English locale applies.
Further still there are other modules in the Test::* namespace to help with all manner of scenarios.
Working to a plan
It may be the case that the precise number of tests in the script is not known or may change frequently. In those situations, specifying the number of tests like use Test::More tests => 3; can become unwieldy or problematic. Instead we can just use Test::More; and then specify the plan later.
One method of doing this is to call plan () as a stand-alone statement. If the number of tests is dependent on an array which is only computed at run time we could write
once the array has been populated.
Another approach is to use done_testing (scalar @array); but as its name suggests this must only be called after the final test has been run. The number of tests can even be omitted entirely here but that removes the check that all the tests expected have indeed run, of course.
Using a harness
If you have installed a module from CPAN you will probably have noticed the test phase running. You can use the same harness on your own test scripts by running the prove command. By default this condenses the results of tests and at the end provides a summary of which tests in which files have failed, how long the run took, etc. eg:
This is particularly useful for larger projects with many scripts/modules each of which has many tests. If prove is run with no arguments it will look for files matching t/*.t and run all of those in sequence.
Test Driven Development
Now that you can test your code you can consider TDD as a methodology. By writing the tests before the code you are setting out what you expect the code to do - it's a formal representation of the specification. Doing so is a skill in itself and many people make a career out of being a tester.