The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Test::MockCommand - provide mock results for external commands

SYNOPSIS

 use Test::Simple tests => 1;
 use Test::MockCommand record => 'commands.db';

 # run 'ls -l', secretly storing its output
 unlink 'testfile.dat';
 my $list = `ls -l`;

 # look up stored command and test its output
 my ($cmd) = Test::MockCommand->find(command => 'ls -l');
 ok $cmd->return_value() eq $list;

 # go into playback mode
 Test::MockCommand->recording(0);

 # run 'ls -l' again while extra file is in the directory
 # should pull result from store, not real life,
 # thus should not see the extra file
 open(my $fh, '>testfile.dat') && close $fh;
 my $again = `ls -l`;
 ok $list eq $again;

DESCRIPTION

Test::MockCommand is a module for recording the output of external commands that are invoked by a perl program, and allowing them to be "played back" later. The module hooks into Perl's core routines, so you simply need to load the module and everything after that is automatic.

Recording command output

Recording is enabled by loading the module with the record parameter:

 use Test::MockCommand record => 'output_filename';

You can also achieve this without module parameters:

 Test::MockCommand->auto_save('output_filename');
 Test::MockCommand->recording(1);

External commands are captured if invoked via `backticks`, qx//, readpipe(), system() or via open() reading or writing to a pipe. 2-way and 3-way IPC is currently unsupported.

Along with the command string itself, the command's input, output, return code and current working directory are recorded.

All commands run will be captured and collected in a database. When the perl script ends, the database will be saved to the given output filename.

You can temporarily turn recording off with Test::MockCommand->recording(0), and turn it back on with Test::MockCommand->recording(1). This only stops new commands being captured. If any commands have already been captured, they will be still be saved when the program ends, regardless of whether recording is stopped.

If you want to avoid saving altogether, use Test::MockCommand->auto_save(undef). If you want to save early, use Test::MockCommand->save().

Defining special handling for commands

In case the default recording is not be enough, you can override the default behaviour with the add_recorder() method. This allows you to add to a chain of recording objects. When a command is being recorded, each recorder is invoked in turn, until one returns a valid result.

The default recorder object is Test::MockCommand::Recorder, which is also designed to be easy to sub-class, saving you from reimplementing core recording functionality. See that class's documentation for further details.

Playing back command output

To play back commands rather than record them, simply load the module without turning on recording mode, and load in a database of previously recorded commands. You can then use backticks, qx//, readpipe(), system(), exec() or open() to a pipe as you normally would. If the command being executed appears in the database, it will be simulated rather than actually run.

You can load a database at the same time you load the module with the playback module parameter:

 use Test::MockCommand playback => 'input_filename';

If no appropriate command is found in the database, a warning is issued and the function acts as normal, running real external commands.

How are commands distinguished?

Commands are stored in the database by a single string, which is composed from the method of invocation, the command being executed and any arguments. For example, `ls -l` becomes the string "readpipe:ls -l" and system('rm', 'file') becomes the string "system:rm file".

In most cases, this is unique. However, you may want to run commands more than once, and the results are different because the environment they run in is different. dir produces different output depending on the directory you are in. date produces different output depending on the time of day. whoami depends on the current user.

To allow for this, multiple command results can be stored under the same string. Each result object is asked in turn if it's the correct object via the matches() method, and a list is collected of objects that say "yes" by returning non-zero values. If more than one object says "yes". the list is sorted by the numeric value of each "yes", and the first object in the sorted list is used as a result. See "matches" in Test::MockCommand::Result for more information.

CLASS METHODS

Test::MockCommand->clear()

Clears the database of stored command results.

Test::MockCommand->merge($filename)

Adds the command results saved in $filename to the current database.

Test::MockCommand->load($filename)

Loads the command results saved in $filename, overwriting any existing results. You can also do this using 'playback' on the import line:

 use Test::MockCommand playback => 'filename.dat';
Test::MockCommand->save()
Test::MockCommand->save($filename)

Saves the current database of command results to the file $filename. If no filename parameter is provided, the auto-save filename is used, should that be set. Will throw an error if no filename is given and there's no auto-save filename set.

Test::MockCommand->add_recorder($recorder)

Registers a recorder object. It will be asked to record commands by having its handle method called. See "handle" in Test::MockCommand::Recorder for more details.

Test::MockCommand->remove_recorder($recorder)

Unregisters a recorder. It will no longer be asked to record commands.

@list = Test::MockCommand->recorders()

Returns a list of all registered recorder objects.

@list = Test::MockCommand->find(%criteria)

Finds command(s) matching the criteria given as parameters. These can be anything the objects implementing the command results know about. The function looks at all relevant command results and calls their matches() method, to see if they think they match your critera. See "matches" in Test::MockCommand::Result for more information. In order to search quickly, the find() function also directly uses the criteria function and command (if you supply them) to cut down the potential list of commands to scan.

@list = Test::MockCommand->all_commands()

Returns a list of all commands in the database.

$is_recording = Test::MockCommand->recording()

Returns non-zero if we're currently recording commands, or zero if we're not.

Test::MockCommand->recording(1)

Starts recording mode. External commands will now be run and their results added to the database.

Test::MockCommand->recording(0)

Stops recording mode. External commands will be played back from the database if possible.

$filename = Test::MockCommand->auto_save()

Returns the auto-save filename, or undef if there is none set.

Test::MockCommand->auto_save($filename)

Sets the filename where the database will automatically written to when the program ends. This will also be the default filename if save() is called without a parameter.

Test::MockCommand->auto_save(undef)

Cancels auto save, nothing will be saved will happen when the program ends.

Test::MockCommand->open_handler($coderef)

As Test::MockCommand globally overrides the open function, this method allows you to set up your own function to handle all the open() calls that don't execute a command. The coderef should take two arguments; the first is an arrayref of arguments to open(), the second is the package name of the calling function. The second parameter should be used in order to qualify the first parameter to open(), if it's a bareword file reference.

As an example, here is a handler that does nothing, simply passing open() calls through to the real open().

 my $passthough_handler = sub {
     my ($args, $pkg) = @_;
     no strict 'refs';
     my $ref = defined($args->[0]) ? Symbol::qualify($args->[0], $pkg) : undef;
     return CORE::open($ref || $args->[0]) if @{$args} == 1;
     return CORE::open($ref || $args->[0], $args->[1]) if @{$args} == 2;
     return CORE::open($ref || $args->[0], $args->[1], splice(@{$args}, 2));
 };
 Test::MockCommand->open_handler($passthrough_handler);
Test::MockCommand->open_handler(undef)

This removes any open() handler. All open() calls will go straight to the real open().

REQUIREMENTS

In order to mock backticks and qx// (both special forms of readpipe()), you need at least Perl version 5.9.5. The ability to override these special operations was only added in this version. See Perl change #29168.

TODO

This module doesn't support mocking IPC::Open2 and IPC::Open3.

If it's detected that a shell is being invoked, rather than just a raw command, and it contains shell redirects (e.g. system("cat </tmp/blah >/tmp/foo");> that external file should be stored in the results too, and either used as identifying material (if it's an input file) or recreated when replayed if it's an output file. The module doesn't currently do this.

Calls to system() can still print output on stdout and stderr, even if this is invisible to perl itself. This should be collected and replicated.

AUTHOR

Stuart Caie, <kyzer@kyzer.me.uk>

COPYRIGHT AND LICENSE

Copyright (C) 2008-2012 by Stuart Caie

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.