http://www.perlmonks.org?node_id=158399

From time to time, I remind people that they need to write tests for their work. My first real experience writing unit tests was when I uploaded CGI::Safe to the CPAN. This was actually fairly daunting due to the complex nature of some of what I was doing, coupled with the fact that I had never written tests before. Now, due to incessant prodding from chromatic, I've gone XP and have started writing unit tests before writing code.

If you've ever read anything about a new programming language, you know that you can't just read about it, you have to do it. Once you actually start getting hands-on experience, then, and only then, do you start to grok the depths (if any) of whatever you're getting into.

If you've not written tests before, the following small test script won't make much sense to you, but I'll point out the highlights that led to revelations for me.

#!/usr/bin/perl use strict; use warnings; $|++; use Test::More tests => 11; use lib ('../lib'); use Foo::Test::Engine; my $site = 'dummy'; my $engine = Foo::Test::Engine->new; ok( defined $engine, 'Engine->new should return something' ); ok( $engine->isa('Foo::Test::Engine'), 'and it should be an Engine object' ); ok( $engine->{_template}->isa('Template'), '$engine->{_template} should be a Template object' ); $engine->_process_page; ok ( ! $engine->error, '_process_page successful'.$engine->error ); my $template = $engine->_get_base_template; ok ( defined $template, 'base template is defined' ); like( $engine->headers, qr/Content-Type: text\/html/, "Headers are being created" ); # we're instantiating another engine object because the tests of priva +te # methods have already processed the output, thus creating double outp +ut my $engine2 = Foo::Test::Engine->new; my $output = $engine2->output; ok( defined $output, "Looks like we got some output" ); unlike( $output, qr/Template Not Found/, "All templates were found" ); my $output_length = length $output; my $template = $engine2->get_template; ok ($template->isa('Template'), "get_template() should return a 'Template' object"); my $test_output; $template->process( \*DATA, {}, \$test_output ) || die $template->erro +r; like( $test_output, qr/good/, '$test_output should have the word "good" in it'); is( length $output, $output_length, 'Length of $output should not chan +ge' ); __DATA__ good

Now, this is very simple. I only have 11 tests, but they are for a fairly consistent API for my Engine object. Some of my tests are dependant on the internals and I dither on whether or not that's a good thing, but the beauty of this is pretty straightforward: many of the tests handle methods that I haven't yet written. I put stubs in there that merely return some hardcoded data. Thus, I can test my API and when I need to actually write the methods, I don't have to worry about how consistent my API is. I've already figured this out.

By having a clearer idea of my API, I can better know what I need to do and I write cleaner code. A case in point was when I first started on this project in the PTE (pre-testing era), I was very proud of the fact that the core script that drives an entire site was only about 40 lines long. Now, by starting over (I wasn't very far into the project) and doing my tests up front, the problems with my previous API shone like beacons. Now, assuming that very little changes, my core script to drive an entire site can be reduced to four lines of code!

use Foo::Test::Engine; my $engine = Foo::Test::Engine->new; print $engine->headers; print $engine->output;

Further, once I find I need to rework the inner magic of my code, I am confident that I can make nice, sweeping changes and still run my tests and know instantly what went wrong. As this is a research project for my work, I've repeatedly been forced to rework things to try new ideas.

Pure bliss :) Tests. Don't leave home without 'em.

Cheers,
Ovid

Join the Perlmonks Setiathome Group or just click on the the link and check out our stats.