On Sun, Mar 06, 2005 at 09:35:07AM -0800, Ofer Nave wrote: > One issue I've always struggled with is how to properly test code that's > not in the form of a module - in other words, scripts. I use the usual > hacky, temporary methods to test my code as I write it or when I find a > bug (pring statements, commenting out things, etc), and occasionally the > perl debugger. But it's unclear to me how one could write formal tests > against a script. > > I've gone through 'fad' periods where I tried to write all code in > modules (even though most of it wasn't reusable) and then have the > script be a simple wrapper that processes command line args and calls > the appropriate functions in the module, but the design never felt right.
That is the best way to do it. Modularity is not so much about reusability but encapsulation. [1] Perhaps the greatest mistake I see programmers making is not putting code into its own routine or library because "its not reusable". More directly applicable to testing is the separation of form and functionality. Let's say one of the things your program does is displays a formatted list of files. There's two things going on there. First, getting the proper list of files. Second, formatting them. From a programming PoV separating these into two routines simplifies the logic and also makes it more flexible. You can take the list of files and format it any way you like. It also makes your testing life much easier. A subroutine which returns a list is very easy to test. You can throw all sorts of different situations at it and just check that it returns the right list. A subroutine which returns a wad of formatted text is much harder to test. Now you need to screen scrape the list out of the text, taking into account any special cases in the formatting. Additionally, you're now testing the listing code and the formatting code simultaneously. When something fails a test was it in the list code or the formatter? Dunno. Finally, formats change. When you change your format you want as few of your tests as possible to break. So this is why whenever possible you want to be testing at the function level not the output level. If you're just testing a program as a black box you only have the output level available. Now, nobody says this means your program has to be split up into a whole bunch of files and become a full fledged module. You can write something like this. #!/usr/bin/perl -w use Getopt::Long; my %Opts; GetOptions(\%Opts, "test"); sub main { return if $Opts{test}; ...the program using the functions below... } main(); sub some_function { ... } sub some_other_function { ... } And then when you want to test the internal functions you can do this: local @ARGV = qw(--test); require "bin/myprogram"; is( some_function(), 'happy' ); And when you want to test the external functionality you can do this: is( `bin/myprogram some args`, 'wibble' ); Or if you want to be super portable you can do this: use Test::Output; local @ARGV = qw(some args); stdout_is( sub { do "bin/myprogram" }, 'wibble' ); Which has the nice side benefit of making sure your code initialzes itself properly between each run rather than just assuming its in a fresh process each time. [1] And often reusability is not so much planned but discovered. And its much easier to discover reusability when things are already well encapsulated than to attempt to see it in a large wad of code.