[Brazosvalley-pm] testing in Perl

James Smith jgsmith at jamesmith.com
Mon Jul 24 11:25:50 PDT 2006


On Jul 24, 2006, at 12:11 PM, Jeremy Fluhmann wrote:

> Does anyone regularly write tests for their code?  I’m finally  
> trying to implement this into my coding practices.
It's been a while now since I was regularly writing Perl, but I did  
put together a framework for myself that is viewable on CPAN:

http://search.cpan.org/src/JSMITH/Gestinanna-0.02/t/lib/My/Builder.pm

That's my personal sub-class of Module::Build.  It uses Pod::Tests  
and a host of other modules to do its magic.

./Build dependencies   (ACTION_dependencies)

This action updates the XML project file with the module dependencies  
based on `use` statements and the methods defined in a particular  
package.

./Build tests  (ACTION_tests)

This action builds all the t/ files that contain the tests.  Tests  
are pulled from each module.  The files are numbered based on the  
dependency graph developed from the data in the XML project file  
created by the above dependencies action.

Each module can also define a setup and cleanup action that gets run  
at the beginning and end of each test file that has that module as a  
dependency.

./Build cover  (ACTION_cover)

This action runs the tests with Devel::Cover to get test coverage  
statistics.

./Build report  (ACTION_report)

This creates a series of HTML pages detailing each module, its  
dependencies, and its methods along with test statistics for each  
method (total tests, passing tests, coverage, documentation).

./Build graph   (ACTION_graph)

This creates a report of which tests ran successfully.  The report is  
an SVG file with a  graphical representation of the modules, their  
dependencies, and their methods with colors denoting completion of  
successful tests.

The modules on the right of the graph depend on the modules on the  
left.  I tended to code so modules turned from red to green going  
from left to right.



What does a typical module look like that makes use of the above  
framework?

package Gestinanna::Util

# some stuff

=head2 path2regex

=begin testing

# path2regex

my %paths = (
     '/' => q{\/},
     '/this' => q{\/this},
     '/*' => q{\/([^\/\@\|\&]+)},
     '//*' => q{\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*([^\/\@\|\&]+)},
     '//*@*' => q{\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*([^\/\@\|\&]+)\@([^ 
\/\@\|\&]+)},
     '//*@name' => q{\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*([^\/\@\|\&]+) 
\@name},
     '//* & //name' => q{(?(?=\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*([^\/\@ 
\|\&]+))(?:\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*name))},
);

foreach my $path (sort keys %paths) {
     is(__PACKAGE__::path2regex($path), $paths{$path}, "path2regex 
($path)");
}

is(__PACKAGE__::path2regex('//*'), $paths{'//*'}, "Cached path2regex 
(//*)");

=end testing

=cut

sub path2regex ($) {
   # ...
}

=head2 path_cmp

( A subset of B == -1, A superset of B == 1, A equivalent to B == 0,  
no intersection or unknown == undef )

=begin testing

# path_cmp

my @paths = (
     [ qw(a a), 1 ],
     [ qw(/this /that), undef ],
     [ qw(/this /that), undef ],
     [ '', '', 1 ],
     [ qw(/this /*), -1 ],
     [ qw(/* /this),  1 ],
     [ q(//foo | //bar), q(//foo | //baz), 0 ],
     [ qw(//*@* //*@name),  1 ],
     [ qw(//*@* //*@name),  1 ],
     [ qw(//*@name //*@*), -1 ],
     [ qw(//foo //bar), undef ],
     [ qw(/foo/bar/baz //bar), undef ],
     [ qw(/foo/bag //bar), undef ],
     [ qw(//bar /foo/bag), undef ],
     [ '/this | /that', '', 1 ],
     [ '', '/this | /that', -1 ],
     [ '/this', '/this | /that', -1 ],
     [ '//bar//* & //foo//*', '/baz/foo/bar/fob', 1],
);

foreach my $path (@paths) {
     is(__PACKAGE__::path_cmp($path->[0], $path->[1]), $path->[2],  
"__PACKAGE__::path_cmp($$path[0], $$path[1])");
}

=end testing

=cut

sub path_cmp ($$) {
   # ...  with calls to path2regex
}

__END__

In the tests, __OBJECT__ (or __OBJECT__(foo) ) is an auto-created  
object of __PACKAGE__ (this allows me to rename a package without  
having to edit all the tests).  __METHOD__ is the current method  
being tested (indicated by the comment right after the =begin testing  
line).  The resulting t/test file:

use lib q{t/lib};
use My::Builder;
# Testing found 1 nodes
use Test::More;
use Module::Build;

BEGIN {
     eval {
         require Gestinanna::Util;
     };

     if($@) {
         plan skip_all => 'Unable to load Gestinanna::Util';
         exit 0;
     }
}

plan no_plan;

my $builder = My::Builder -> current;

my %objects;

# method: path2regex

$builder -> begin_tests('path2regex');


{
     undef $main::_STDOUT_;
     undef $main::_STDERR_;
#line 187

my %paths = (
     '/' => q{\/},
     '/this' => q{\/this},
     '/*' => q{\/([^\/\@\|\&]+)},
     '//*' => q{\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*([^\/\@\|\&]+)},
     '//*@*' => q{\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*([^\/\@\|\&]+)\@([^ 
\/\@\|\&]+)},
     '//*@name' => q{\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*([^\/\@\|\&]+) 
\@name},
     '//* & //name' => q{(?(?=\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*([^\/\@ 
\|\&]+))(?:\/+(?:([^\/\@\|\&]+)\/+)*(?:\/)*name))},
);

foreach my $path (sort keys %paths) {
     is(Gestinanna::Util::path2regex($path), $paths{$path},  
"path2regex($path)");
}

is(Gestinanna::Util::path2regex('//*'), $paths{'//*'}, "Cached  
path2regex(//*)");


     undef $main::_STDOUT_;
     undef $main::_STDERR_;
}


$builder -> end_tests('path2regex');

# method: path_cmp

$builder -> begin_tests('path_cmp');


{
     undef $main::_STDOUT_;
     undef $main::_STDERR_;
#line 277

my @paths = (
     [ qw(a a), 1 ],
     [ qw(/this /that), undef ],
     [ qw(/this /that), undef ],
     [ '', '', 1 ],
     [ qw(/this /*), -1 ],
     [ qw(/* /this),  1 ],
     [ q(//foo | //bar), q(//foo | //baz), 0 ],
     [ qw(//*@* //*@name),  1 ],
     [ qw(//*@* //*@name),  1 ],
     [ qw(//*@name //*@*), -1 ],
     [ qw(//foo //bar), undef ],
     [ qw(/foo/bar/baz //bar), undef ],
     [ qw(/foo/bag //bar), undef ],
     [ qw(//bar /foo/bag), undef ],
     [ '/this | /that', '', 1 ],
     [ '', '/this | /that', -1 ],
     [ '/this', '/this | /that', -1 ],
     [ '//bar//* & //foo//*', '/baz/foo/bar/fob', 1],
);

foreach my $path (@paths) {
     is(Gestinanna::Util::path_cmp($path->[0], $path->[1]), $path-> 
[2], "Gestinanna::Util::path_cmp($$path[0], $$path[1])");
}


     undef $main::_STDOUT_;
     undef $main::_STDERR_;
}


$builder -> end_tests('path_cmp');
# record test results for report
$builder -> record_test_details('Gestinanna::Util');
my $tester = Test::More -> builder;
if($tester -> current_test == 0) {
     $tester -> skip_all( 'No tests defined' );
}

__END__


This test framework allows me to start with the documentation and  
then proceed to testing.  I can put all of the tests together with  
the human description of what a method is supposed to do before I  
ever write a line of code.  I can pass the module off to someone else  
at any point and know they have all the tests that I would have had.   
The reporting features of the Module::Build subclass allow me to put  
together nice progress reports to illustrate progress.

I have the method dependencies within a module hand coded in the XML  
file that describes the module dependencies.  I could (and probably  
should) have then listed in a comment in the testing section.   
Something like:

=begin testing

# path_cmp
# depends on: path2regex

...

=end testing

That way, the information is all in one place and the XML file should  
never need editing.

-- Jim
-------------- next part --------------
A non-text attachment was scrubbed...
Name: smime.p7s
Type: application/pkcs7-signature
Size: 2425 bytes
Desc: not available
Url : http://mail.pm.org/pipermail/brazosvalley-pm/attachments/20060724/27ac0757/attachment.bin 


More information about the Brazosvalley-pm mailing list