views:

585

answers:

3

I need to add unit testing to some old scripts, the scripts are all basically in the following form:

#!/usr/bin/perl

# Main code
foo();
bar();

# subs
sub foo {

}
sub bar {

}

If I try to 'require' this code in a unit test, the main section of the code will run, where as I want to be able to just test "foo" in isolation.

Is there any way to do this without moving foo,bar into a seperate .pm file?

+12  A: 

Assuming you have no security concerns, wrap it in a sub { ... } and eval it:

use File::Slurp "read_file";
eval "package Script; sub {" . read_file("script") . "}";

is(Script::foo(), "foo");

(taking care that the eval isn't in scope of any lexicals that would be closed over by the script).

ysth
Since one has to run code to test it, there's no special security concern with using eval STRING here.
Schwern
+11  A: 

Another common trick for unit testing scripts is to wrap the body of their code into a 'caller' block:

#!/usr/bin/perl

use strict;
use warnings;

unless (caller) {
    # startup code
}

sub foo { ... }

When run from the command line, cron, a bash script, etc., it runs normally. However, if you load it from another Perl program, the "unless (caller) {...}" code does not run. Then in your test program, declare a namespace (since the script is probably running code in package main::) and 'do' the script.

#!/usr/bin/perl

package Tests::Script;   # avoid the Test:: namespace to avoid conflicts
                         # with testing modules
use strict;
use warnings;

do 'some_script' or die "Cannot (do 'some_script'): $!";

# write your tests

'do' is more efficient than eval and fairly clean for this.

Another trick for testing scripts is to use Expect. This is cleaner, but is also harder to use and it won't let you override anything within the script if you need to mock anything up.

Ovid
If there is a remote possibility that somebody will package your script with PAR (i.e. "pp -o binary script.pl; ./binary") or actually eval it and expect it to run, then *please* don't do this. This gave me lots of worries with perlcritic when I was preparing my YAPC::EU talk.
tsee
To be fair, I don't worry in the slightest about PAR. Perl 5 has too many flaws for me to remember all of the special cases, particularly for non-core code :(
Ovid
+7  A: 

Ahh, the old "how do I unit test a program" question. The simplest trick is to put this in your program before it starts doing things:

return 1 unless $0 eq __FILE__;

__FILE__ is the current source file. $0 is the name of the program being run. If they are the same, your code is being executed as a program. If they're different, it's being loaded as a library.

That's enough to let you start unit testing the subroutines inside your program.

require "some/program";
...and test...

Next step is to move all the code outside a subroutine into main, then you can do this:

main() if $0 eq __FILE__;

and now you can test main() just like any other subroutine.

Once that's done you can start contemplating moving the program's subroutines out into their own real libraries.

Schwern
The same gotcha I point out in the comment to Ovid's solution applies here: Try packaging that with PAR::Packer or similar tools.
tsee
@tsee I'll blow up that bridge when it comes. ysth's trick is clever though and seems to get around the problem without having to edit the code at all.
Schwern