Test Driven Development in Perl

I recently took a class that included a lot of Test Driven Development (TDD) in Ruby, but I work at a Perl shop. As I slowly became more obsessed with the concept of TDD, especially the unit testing pieces, I discovered that available tutorials didn’t teach you what you really needed to get started – although the information is generally available with a few web searches. For instance, the CPAN Test::Tutorial page does not cover the ‘prove’ command, which ultimately allows you to run tests. Nor does it mention App::Prove. I’m not sure that the Test::Tutorial page is the right place to mention those, although it might seem reasonable.

Regardless, this tutorial will cover all of the steps on an Ubuntu box, starting with the initialization of a git repo, a creation of all tests before development, verifying failure, and finally making all of the steps pass. Later I might cover more expansive items.

First things first, going to create a repo:

mkdir -p ~/Documents/devel/test/tdd/
cd ~/Documents/devel/test/tdd/
git init

Now the fun begins, first the prove command. This is what will run our unit tests. After the first go, I get:

psalcido@psalcido-ubuntu:~/Documents/devel/test/tdd$ prove
No tests named and 't' directory not found at /usr/share/perl/5.14/App/Prove.pm line 527

So I create the t directory and rerun:

psalcido@psalcido-ubuntu:~/Documents/devel/test/tdd$ mkdir t
psalcido@psalcido-ubuntu:~/Documents/devel/test/tdd$ prove
Files=0, Tests=0,  0 wallclock secs ( 0.00 usr +  0.00 sys =  0.00 CPU)
Result: NOTESTS

I’m going to create a simple programming problem that is simple enough to implement quickly, and I’ve decided on Project Euler Problem 1:

If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.

Find the sum of all the multiples of 3 or 5 below 1000.

I’m not going to show the code that I used to complete this task, as it’d be ruining the fun for countless other developers. I am going to be expecting a class file ‘ProjectEuler::Problem1’, with the subroutine ‘process’. I’ll write the test cases that represent these first expectations, in the file t/00_projectueler_problem1.t:

#! /usr/bin/perl -w

use Test::More tests => 2;

BEGIN { use_ok('ProjectEuler::Problem1') }

TODO: {
        can_ok( 'ProjectEuler::Problem1','process');
        # OR ok( ProjectEuler::Problem1::can('process') , "ProjectEuler Problem 1 can execute the process command." );
}

Most of the details here are clarifed at Test::Tutorial, so I shouldn’t get into too much detail. With my test written, I can try to prove my code:

psalcido@psalcido-ubuntu:~/Documents/devel/test/tdd$ prove
t/00_projecteuler_problem1.t .. 1/2 
#   Failed test 'use ProjectEuler::Problem1;'
#   at t/00_projecteuler_problem1.t line 5.
#     Tried to use 'ProjectEuler::Problem1'.
#     Error:  Can't locate ProjectEuler/Problem1.pm in @INC (@INC contains: /etc/perl /usr/local/lib/perl/5.14.2 /usr/local/share/perl/5.14.2 /usr/lib/perl5 /usr/share/perl5 /usr/lib/perl/5.14 /usr/share/perl/5.14 /usr/local/lib/site_perl .) at (eval 4) line 2.
# BEGIN failed--compilation aborted at (eval 4) line 2.

#   Failed test 'ProjectEuler Problem 1 can execute the process command.'
#   at t/00_projecteuler_problem1.t line 7.
# Looks like you failed 2 tests of 2.
t/00_projecteuler_problem1.t .. Dubious, test returned 2 (wstat 512, 0x200)
Failed 2/2 subtests 

Test Summary Report
-------------------
t/00_projecteuler_problem1.t (Wstat: 512 Tests: 2 Failed: 2)
  Failed tests:  1-2
  Non-zero exit status: 2
Files=1, Tests=2,  0 wallclock secs ( 0.02 usr +  0.01 sys =  0.03 CPU)
Result: FAIL

Now I have a programming contract that I can keep. I have designed the way that my package is called, and the name of the subroutine that will be used to run the test. That is only the first part of the contract that I’m going to write up before I start development, but that brings up one of the key things about writing all of your tests before you start.

As an aside, there are two camps in TDD. One is to write one test and make it pass at a time. Another group says to write all of your tests before you start, and only then should you begin development. Neither is correct, both are just flavors of an idea that should become key in your future development techniques: write tests early and often.

OK, now that I’ve got the test bundle working, I’m going to finish up writing some additional tests into the file and make them work as well. These include actual expected results.

#! /usr/bin/perl -w

use Test::More tests => 7;

BEGIN { use_ok('ProjectEuler::Problem1') }

TODO: {
        can_ok( 'ProjectEuler::Problem1','process');
        cmp_ok( ProjectEuler::Problem1::process( 0 ) , '==' , 0 , "Running 0 results in 0." );
        cmp_ok( ProjectEuler::Problem1::process( 3 ) , '==' , 3 , "Running 3 results in 3." );
        cmp_ok( ProjectEuler::Problem1::process( 5 ) , '==' , 8 , "Running 5 results in 8." );
        cmp_ok( ProjectEuler::Problem1::process( 10 ) , '==' , 33 , "Running 10 results in 33." );
        cmp_ok( ProjectEuler::Problem1::process( -5 ) , '==' , 0 , "Running -5 results in 0." );
}

Now I’ve further clarified my contract, negative number result in 0, and 0 results in 0. Further, I’ve tested some values along the way for positive tests as well (I hope my math is right). You can run prove now, and see the 255 error that you might get back, and it will fail on the second test completely, resulting in only seven tests run. At this point I also did a code commit.

Now that I’m coding, I create the ProjectEuler directory, and I create the file Project1.pm under it. The following contents are added:

package ProjectEuler::Problem1;

sub process { }

1;

Now my prove should look different:

psalcido@psalcido-ubuntu:~/Documents/devel/test/tdd$ prove
t/00_projecteuler_problem1.t .. 1/7 Use of uninitialized value $got in numeric eq (==) at (eval in cmp_ok) t/00_projecteuler_problem1.t line 9.
Use of uninitialized value $got in numeric eq (==) at (eval in cmp_ok) t/00_projecteuler_problem1.t line 10.

#   Failed test 'Running 3 results in 3.'
#   at t/00_projecteuler_problem1.t line 10.
Use of uninitialized value $val in addition (+) at /usr/share/perl/5.14/Test/Builder.pm line 917.
#          got: undef
#     expected: 3
Use of uninitialized value $got in numeric eq (==) at (eval in cmp_ok) t/00_projecteuler_problem1.t line 11.

#   Failed test 'Running 5 results in 8.'
#   at t/00_projecteuler_problem1.t line 11.
Use of uninitialized value $val in addition (+) at /usr/share/perl/5.14/Test/Builder.pm line 917.
#          got: undef
#     expected: 8
Use of uninitialized value $got in numeric eq (==) at (eval in cmp_ok) t/00_projecteuler_problem1.t line 12.

#   Failed test 'Running 10 results in 33.'
#   at t/00_projecteuler_problem1.t line 12.
Use of uninitialized value $val in addition (+) at /usr/share/perl/5.14/Test/Builder.pm line 917.
#          got: undef
#     expected: 33
Use of uninitialized value $got in numeric eq (==) at (eval in cmp_ok) t/00_projecteuler_problem1.t line 13.
# Looks like you failed 3 tests of 7.
t/00_projecteuler_problem1.t .. Dubious, test returned 3 (wstat 768, 0x300)
Failed 3/7 subtests 

Test Summary Report
-------------------
t/00_projecteuler_problem1.t (Wstat: 768 Tests: 7 Failed: 3)
  Failed tests:  4-6
  Non-zero exit status: 3
Files=1, Tests=7,  0 wallclock secs ( 0.03 usr  0.00 sys +  0.01 cusr  0.00 csys =  0.04 CPU)
Result: FAIL

Now all of a sudden, I’m passing a decent number of tests. This is unfortunate, because I don’t like the == operator undef matching zero, so I’m going to try something different. In Test::More, the ‘is’ subroutine is also available, so I changed my file to match:

#! /usr/bin/perl -w

use Test::More tests => 7;

BEGIN { use_ok('ProjectEuler::Problem1') }

TODO: {
        can_ok( 'ProjectEuler::Problem1','process');
        is( ProjectEuler::Problem1::process( 0 ) , 0 );
        is( ProjectEuler::Problem1::process( 3 ) , 3 );
        is( ProjectEuler::Problem1::process( 5 ) , 8 );
        is( ProjectEuler::Problem1::process( 10 ) , 33 );
        is( ProjectEuler::Problem1::process( -5 ) , 0 );
}

Which now breaks five of the tests, which is what I expect when returning undef.

In the next step, I do two things. First I add in a little trickery that python developers are often so proud of, the ability to call a package as a script if it is called from the command line:

package ProjectEuler::Problem1;

sub process {
}

# The following will be called if this file is run as a script.

package main;

use Getopt::Long;

my ($input) = ( );

GetOptions(
        "input=i" => \$input
);

$input = int($input || 0);

if ( $input ) {
        print ProjectEuler::Problem1::process($input)."\n";
}

1;

This step makes manual testing that much easier, and will allow me to demonstrate process working. After I have developed the process functionality properly (Not going to show it, sorry, even though it is a very simple problem) , I go ahead and rerun my tests again to make sure all looks well.

psalcido@psalcido-ubuntu:~/Documents/devel/test/tdd$ prove
t/00_projecteuler_problem1.t .. ok   
All tests successful.
Files=1, Tests=7,  0 wallclock secs ( 0.03 usr  0.00 sys +  0.02 cusr  0.00 csys =  0.05 CPU)
Result: PASS

Hopefully this gets you through the earliest steps of designing tests and running them for Perl, as it actually took me a decent amount of time to find all of this information and how to get it together in one place. Perhaps you also agree with the concept of a ‘design contract’, even if you are making it with yourself, and the ability to test without having to repeatedly change code and add debug statements to your classes. There is still a lot to learn, like how to make sure that your unit tests can test only the code that you are working on without depending on the behavior of other classes, then expanding into integration and business test cases. I hope to have tutorials for these in perl, and perhaps expanding into other languages, soon.

Advertisements

3 comments

  1. […] I spoke about tests and test driven development in perl, and now I continue with discussion of MockObject and MockModule; why it’s important, and how […]

  2. thanks for this tutorial. it was very helpful for me.

  3. thanks for your tutorial. it was very helpful for me…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: