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.

Reply via email to