27

I have written a small Perl script and now I would like to create a test suite for it. I thought it would be nice to be able to use the script as a module, import the subs defined in the script and test these. Is there a way to have the script both standalone Perl script and Perl module? (I do not want to split the script into a separate module and the “executable”, since I plan to distribute the script as a single file.)

Or is there a better way to test the script?

brian d foy
  • 129,424
  • 31
  • 207
  • 592
zoul
  • 102,279
  • 44
  • 260
  • 354

7 Answers7

33

Since brian seems to be asleep right now, here's a pointer to what he calls modulinos. This is basically a recipe for building scripts that act like modules or modules that can be run like scripts. Sounds exactly like what you are looking for.

Mastering Perl is definitely a book worth reading (and buying).

Colin Pickard
  • 45,724
  • 13
  • 98
  • 148
innaM
  • 47,505
  • 4
  • 67
  • 87
  • I think there is a convenience in making a module act like a script, but I don't see the value in making a script act like a module. At least not yet.... – Joe Casadonte Jan 22 '09 at 20:16
  • The answer is in the question (not your question, but the original one). It is much easier to unit-test a module than to unit-test a script – innaM Jan 22 '09 at 20:37
  • 3
    Not asleep, just working :) The advantage of having a script as a module is that you can break it into pieces for unit testing. Otherwise, you only get to run the whole script. – brian d foy Jan 22 '09 at 22:50
  • This is great! I've been wondering how to do this. Would it also be valid to name a script with a .pl extension, and put this line in the top: "return 1 if ( caller() );" That way, the script can run normally, and then my testing script can "require 'myscript.pl'" and test each function. Or is that bad practice? – BrianH Feb 05 '10 at 17:22
  • the links had rotted; I've had a go at replacing them – Colin Pickard Jun 14 '12 at 14:39
  • I think it's perfectly fine, to name the script with a .pl extension. I personally did something similar to `return 1 if (caller());` to my script once I discovered this page. Thanks all for the great help! – Speeddymon Dec 13 '17 at 00:25
8

(I do not want to split the script into a separate module and the “executable”, since I plan to distribute the script as a single file.)

Most people stitch files together like this at build time. App::Ack on the CPAN is an example of something that can be built like this.

If you really want to test your application properly, you need to put the functionality in a module, and then write a Test::More-based test script that exercises the functions that the module provides. Then the actual script is just a thin wrapper around the module, usually something like:

#!/usr/bin/env perl
use Your::Class;
Your::Class->new(args => \@ARGV)->run;

See also: MooseX::Getopt.

Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129
jrockway
  • 42,082
  • 9
  • 61
  • 86
4

I want to do the same thing. Is there a reason not to do the following? (I'm not a perl expert, the solution seems to work fine for me, though.):

in the beginning of the script I ask for a switch ("-test") or whatever and than branch to a sub like this:

my $myargs = CmdLineOptions->new( args=>\@ARGV );
if ($myargs->was_given(option=>"-test"))    { &tests; }

sub tests   {
    require "Test/More.pm";
    Test::More->import('no_plan');

    is(1,1,"my tests go here");
    exit;
}

(by using require and import i suppress the '# No tests run!' message i get when using the 'use Test::More' without doing any tests. I guess it also reduces the overhead.)

Dirk
  • 41
  • 1
2

It depends on whether you want to test the script itself, or test the subs that make up the script. If you want to test the script, then an external test would be more appropriate, e.g. a shell script. If you want to test the functions that make up the script, then you can either write those tests as more functions within the script or refactor the elements into a Perl module and test the module (which you say you don't want to do).

If the script is small enough, then refactoring might not be necessary. Simply add a '-test' command line argument and call into a test sub, which in turn tests everything else. If doing this, I like to print out a progress indicator of some kind (e.g. a '.' for every test that passes).

If the script is more complex, though, you may want to consider refactoring bits into one or more modules and testing them with Test::Simple or Test::More.

Joe Casadonte
  • 15,888
  • 11
  • 45
  • 57
1

Why not writing a test suite in a shell script, with the Perl script called like any other shell command ?

brian d foy
  • 129,424
  • 31
  • 207
  • 592
mouviciel
  • 66,855
  • 13
  • 106
  • 140
  • Yes, that’s surely an option, but using the subs directly seems much more convenient to me. – zoul Jan 22 '09 at 11:29
0

Since I just wanted to test one sub, I used the hacky solution of extracting that sub into a module with a perl one-liner, which is easy to understand and doesn't require to install any modules (I extract the paragraph with the sub name which is of course brittle). So in my Makefile I have:

MyModule.pm: my_script.pl
    perl -00 -lnE 'if(/^\s*sub my_sub \{/){say "package MyModule;";say;say "1;";}' my_script.pl >|MyModule.pm
Johannes Riecken
  • 2,301
  • 16
  • 17
0

While making some CGI style perl scripts remotely testable (without looping thru CGI-test modules) I ended up doing something like this:

bin/myscript.pl:

use …;

do {
  return 1 if $ENV{INCLUDED_AS_MODULE};

  … original script without the subs …
};

sub some_sub {
 …
}
sub …
sub …

t/mytest.t:

use Test::More;
use FindBin qw/$Bin/;

BEGIN {
  $ENV{INCLUDED_AS_MODULE} = 1;
  my $filename = "$Bin/../bin/myscript.pl";
  do $filename or die "Unable to open '$filename': $! ($@)";
}

ok some_sub(), 'some sub returns true';
…
done_testing;

This imports the subs into the main namespace of the test and can be tested easily.

EDIT: And after some more searching for what I was looking for, I realize modulinos are basically what I did, but avoiding communicating via ENV, putting all your code in a method and just checking wether there is a caller():

https://www.perlmonks.org/bare/?node_id=537377 (link in a previous answer did not seem to work)

nicomen
  • 1,183
  • 7
  • 16