Testing App::Cmd

As I mentioned last week, I’ve been working on a little reading list management tool. I’m using App::Cmd as the framework, which has been a somewhat frustrating but ultimately worthwhile choice.

I first learned of App::Cmd at the Pittsburgh Perl Workshop, through a talk given by the module’s author Ricardo Signes. The slides from that talk are about the best App::Cmd documentation out there, although App::Cmd::Tutorial also has some useful bits, and the module documentation itself isn’t bad by any means, just a little light on example code.

For BookList I didn’t do a lot of up-front design work – I was basically designing as I went and learning App::Cmd at the same time. As a result, I ended up implementing several commands with only the most rudimentary tests – basically, if the command module loaded, I was happy. Since BookList is getting towards a 0.1 level of functionality and I’d like to release it, I spent the past couple days renewing my appreciation of “test first” development by going back and writing all the tests I skipped over the first time through.

Because of the way the framework works, testing App::Cmd code is a bit different than testing library-level routines. The talk I pointed to above has a section on testing (around slide 115, for those of you who want to check it out), which references a nifty looking module called Test::App::Cmd. Unfortunately, the module doesn’t appear to be publicly available yet. (This might have been mentioned at the talk; I don’t recall. Any status updates are welcomed.)

After poking around on CPAN, trying to find something to provide some scaffolding for my tests, I ended up finding a module called Test::Output. I used that and the sample test code in the talk to write a bunch of tests that looked like this:

use Test::More     qw/ no_plan /;
use Test::Output qw/ stdout_from /;

use Booklist::Cmd;

my $error;
my $stdout = do {
local @ARGV = ( 'authors' );
stdout_from( sub {
eval { Booklist::Cmd->run ; 1 } or $error = $@;
} );

like $stdout , qr/^### #bk author/ , 'see expected header';
ok ! $error;

and while that mostly worked out okay, I ran into trouble with some of my error-handling code. When writing libraries, especially for my personal use, I tend towards the low-budget “croak() and let the caller deal” method of exception handling. But for something that’s going to be more application-level, I wanted to print messages to STDERR and then exit() with a non-zero status. This doesn’t play well with Test::More or Test::Output, neither of which trap calls to exit().

After a bit more CPAN searching, I found Test::Trap which is excellent for testing App::Cmd code. The above code rewritten to use Test::Trap looks like this:

use Test::More    qw/ no_plan /;
use Test::Trap qw/ trap $trap /;

use Booklist::Cmd;

trap {
local @ARGV = ( 'authors' );

$trap->leaveby_is( 'return' , 'exit normally' );
$trap->stdout_like( qr/^### #bk author/ , 'see expected header' );
$trap->stderr_nok( 'nothing on stderr' );

That second code block is a lot cleaner and easier to understand than the first one (not to mention being fewer LOC while doing an additional test), and the accessor/comparison methods (leaveby_is, stdout_like, etc.) from Test::Trap are both easy to write and (if you know anything at all about idiomatic Perl testing code) trivial to understand.

I’m going back now and rewriting all my previous Test::Output tests to use Test::Trap – but I’ve also learned another lesson here: my last test is currently failing, because I wrote it beforeimplementing the command it tests.