Writing Testable Programs

     

Not every useful piece of Perl code fits in its own module. There's a wealth of worthwhile code in scripts and programs. You know the rule: if it's worth using, it's worth testing. How do you test them? Write them to be as testable as possible.


Note: Simple, well- factored code is easier to test in isolation. Improving the design of your code is just one of the benefits of writing testable code .

How do I do that?

Imagine that you have a program that applies filters to files given on the command line, sorting and manipulating them before printing them. Save the following file as filefilter.pl :

 #!perl          use strict;     use warnings;          main( @ARGV ) unless caller(  );          sub main     {         die "Usage:\n 
 #!perl use strict; use warnings; main( @ARGV ) unless caller( ); sub main { die "Usage:\n$0 <command> [file_pattern]\n" unless @_; my $command = shift; my $command_sub = main->can( "cmd_$command" ); die "Unknown command '$command'\n" unless $command_sub; print join( "\n", $command_sub->( @_ ) ); } sub sort_by_time { map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [ $_, -M $_ ] } @_ } sub cmd_latest { (sort_by_time( @_ ) )[0]; } sub cmd_dirs { grep { -d $_ } @_; } # return true 1; 
<command> [file_pattern]\n" unless @_; my $command = shift; my $command_sub = main->can( "cmd_$command" ); die "Unknown command '$command'\n" unless $command_sub; print join( "\n", $command_sub->( @_ ) ); } sub sort_by_time { map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [ $_, -M $_ ] } @_ } sub cmd_latest { (sort_by_time( @_ ) )[0]; } sub cmd_dirs { grep { -d $_ } @_; } # return true 1;

Testing this properly requires having some test files in the filesystem or mocking Perl's file access operators ("Overriding Built-ins" in Chapter 5). The former is easier. Save the following program as make_test_files.pl :


Note: filefilter.pl ends with "1;" so that the require()will succeed.See perldoc -f require to learn more .
 #!perl          use strict;     use warnings;          use Fatal qw( mkdir open close );     use File::Spec::Functions;          mkdir( 'music_history' ) unless -d 'music_history';          for my $subdir (qw( handel vivaldi telemann ))     {         my $dir = catdir( 'music_history', $subdir );         mkdir( $dir ) unless -d $dir;     }          sleep 1;          for my $period (qw( baroque classical ))     {         open( my $fh, '>', catfile( 'music_history', $period ));         print $fh '18th century';         close $fh;         sleep 1;     } 

Save the following test as test_filefilter.t :

 #!perl          use strict;     use warnings;          use Test::More tests => 5;     use Test::Exception;          use File::Spec::Functions;          ok( require( 'filefilter.pl' ), 'loaded file okay' ) or exit;          throws_ok { main(  ) } qr/Usage:/,         'main(  ) should give a usage error without any arguments';          throws_ok { main( 'bad command' ) } qr/Unknown command 'bad command'/,         '... or with a bad command given';          my @directories =     (         'music_history',         map { catdir( 'music_history', $_ ) } qw( handel vivaldi telemann )     );          my @files = map { catfile( 'music_history', $_ ) } qw( baroque classical );          is_deeply( [ cmd_dirs( @directories, @files ) ], \@directories,         'dirs command should return only directories' );          is( cmd_latest( @files ), catfile(qw( music_history classical )),         'latest command should return most recently modified file' ); 


Note: Baroque preceded Classical, of course .

Run make_test_files.pl and then run test_filefilter.t with prove :

 $  prove test_filefilter.t  test_filefilter....ok     All tests successful.     Files=1, Tests=5,  0 wallclock secs ( 0.08 cusr +  0.02 csys =  0.10 CPU 

What just happened ?

The problem with testing Perl programs that expect to run directly from the command line is loading them in the test file without actually running them. The strange first code line of filefilter.pl accomplishes this. The caller( ) operator returns information about the code that called the currently executing code. When run directly from the command line, there's no caller information, and the program passes its arguments to the main( ) subroutine. When run from the test script, the program has caller information, so it does nothing.

The rest of the program is straightforward.

The test file requires the presence of some files and directories to test against. Normally, creating test data from within the test itself works, but in this case, part of the filter program relies on Perl's behavior when checking the last modification time of a file. Because Perl reports this time relative to the time at which the test started, it's much easier to create these files before running the test. Normally, this might be part of the build step. Here, it's a separate program: make_test_files.pl . The sleep line attempts to ensure that enough time passes between the Baroque and the Classical periods that the filesystem can tell their creation times apart. [*]

[*] Sure, that's 150 years of musical history, but computers don't have much culture.

The test uses require( ) to load the program. Test::More::require_ok( ) is inappropriate here because it expects to load modules, not programs. The rest of the test is straightforward.


Note: The test is incomplete, though; how would you test the printing behavior of main()?

What about...

Q:

What if I run this code on a filesystem that can't tell the difference between the modification times of baroque and classical ?

A:

That's one purpose of the test. If the test fails, you might need to modify filefilter.pl to take that into account. Start by increasing the value of the sleep call in make_test_files.pl and see what the limits of your filesystem are.

Q:

What if the program being tested calls exit( ) or does something otherwise scary?

A:

Override it (see "Overriding Built-ins" in Chapter 5).

Q:

When would you do this instead of running filefilter.pl as a separate program (see "Testing Programs," next )?

A:

This technique makes it easier to test the program's internals. Running it as a separate program means that your test has to treat the entire program as a black box. Note that the test here doesn't have to parse the program's output; it handles the list returned from cmd_dirs( ) , and the scalar returned from cmd_latest( ) as normal Perl data structures.



Perl Testing. A Developer's Notebook
Perl Testing: A Developers Notebook
ISBN: 0596100922
EAN: 2147483647
Year: 2003
Pages: 107

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net