Skip navigation

Monthly Archives: November 2009

I was reminded of the epigram “If it’s broken, don’t fix it.”. My problem with this is that people don’t apply the contrapositive. So many just leave things that don’t work around, and that plain sucks.

Don’t suffer brokenness. Fix it.

There are tests that you want to have around, but not be part of the standard test run prior to installation of a distribution. Probably the biggest set of these in publicly available Perl distributions is “author” tests (manifest checks, code and POD coverage, etc.), but there are other things such as live database tests that should not block installation. The most common way of disabling tests is via checking an environment variable in a test file:


1 if ( not $ENV{TEST_AUTHOR}) {
2     plan skip_all => Author test. Set $ENV{TEST_AUTHOR} to a true value to run.;
3 }

On the Perl::Critic project, we ran into problems with this. We got failures when people had the environment variable set for their own purposes. The code that checked for enabling author tests also turned them on if there was a .svn directory around, so, of course, we got a complaint when someone grabbed the source from the Perl::Critic repository. *sigh*

The proper way to enable author tests is to require the user to explicitly say that she wants them to be run.

The first thing to do is to segregate the non-standard tests from the regular ones so that the standard testing tools don’t run them by default. At the 2008 QA hackathon in Oslo it was decided that these tests belong under the “xt” directory in a distribution; there was no mandate about substructure. I’ve been using “xt/author” for my author tests.

The second step is to create a “authortest” build target. Since I use Module::Build as my build tool, this example will use that. The way to create a custom target in Module::Build is to create a method on a Module::Build subclass with a name that starts with “ACTION_”. Here’s my standard “authortest” code:


 1 sub ACTION_authortest {
 2     my ($self) = @_;
 3
 4     $self->depends_on(build);
 5     $self->depends_on(manifest);
 6     $self->depends_on(distmeta);
 7
 8     $self->test_files( qw< t xt/author > );
 9     $self->recursive_test_files(1);
10
11     $self->depends_on(test);
12
13     return;
14 }

First this asks Module::Build to ensure that the build, manifest, and distmeta actions have been run because some of the tests depend upon the MANIFEST and META.yml files being around and I don’t check those into source control. Line 8 tells Module::Build to look in the standard “t” directory plus “xt/author” for tests. Line 9 says that the test files may not be directly in these subdirectories. Finally, it asks for the regular test action to be run.

Using this, running the standard “./Build test” command doesn’t run author tests, no matter what the user’s environment is. And simply running “./Build authortest” causes all the tests to run. Simple, yet explicit.

To ensure that nothing gets released without the author tests being run, I change the distdir action to require the author tests be run:


1 sub ACTION_distdir {
2     my ($self) = @_;
3
4     $self->depends_on(authortest);
5
6     return $self->SUPER::ACTION_distdir();
7 }

Don’t put the implementation of your program in your program; put it in a module instead. There are at least three reasons for doing things this way:

  • Your code is more testable. You can feed in arbitrary command lines and check the exit code.
  • Your code is reusable. If you need to use your program from another Perl program, there’s no need to use system, backticks, or IPC::System::Simple, just use your module and call the implementing subroutine.
  • Your programs can be better managed by the standard Perl toolchain. Program versions are not checked by CPAN, et. al. so if you create a new version of an existing program, it won’t be upgraded. If all of your code is in a module, then version changes will cause updates to be installed.

A simple way of doing this would be to have a module that looks like


 1 package ProgramImplementation;
 2
 3 use utf8;
 4 use 5.010;
 5
 6 use strict;
 7 use warnings;
 8
 9 use Exporter qw< import >;
10
11 our @EXPORT_OK = qw< run >;
12
13
14 sub run {
15     my (@argv) = @_;
16
17     …
18
19     return 0;
20 }
21
22 1;

and then use that like this:


 1 use utf8;
 2 use 5.010;
 3
 4 use strict;
 5 use warnings;
 6
 7 use ProgramImplementation qw< run >;
 8
 9 return 1 if caller;
10 exit run(@ARGV);

Having the “return 1 if caller” in there means that the program can be required without causing the requiring program to exit. (Whether you ought to actually do that is another matter.)

If you want things to be even more reusable, make your program implementation an object with attributes for the standard file handles:


 1 package ObjectProgramImplementation;
 2
 3 use utf8;
 4 use 5.010;
 5
 6 use Moose;      # Or your favorite object module.
 7
 8 has stdout => (
 9     isa => FileHandle,
10     …
11     default => sub { \*STDOUT },
12 );
13
14 has stderr => (
15     isa => FileHandle,
16     …
17     default => sub { \*STDERR },
18 );
19
20
21 sub run {
22     my ($self, @argv) = @_;
23
24     say { $self->stdout() } Hello there!;
25     …
26
27     return 0;
28 }
29
30 1;

Your regular program would then look like this:


 1 use utf8;
 2 use 5.010;
 3
 4 use strict;
 5 use warnings;
 6
 7 use ObjectProgramImplementation qw< >;
 8
 9 return 1 if caller;
10 exit ObjectProgramImplementation->new()->run(@ARGV);

But you can have other uses like


 1 my $stdout;
 2 my $stderr;
 3 open my $stdout_handle, >, \$stdout;
 4 open my $stderr_handle, >, \$stderr;
 5
 6 my $program =
 7     ObjectProgramImplementation->new(stdout => $stdout, stderr => $stderr);
 8 my $exit_code = $program->run(@ARGV);
 9
10 # Do something with $stdout/$stderr