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

NAME

Test::CallFlow - trivial planning of sub call flows for fast unit test writing.

VERSION

Version 0.03

SYNOPSIS

Mock packages for planning expected interactions in tests:

    use Test::CallFlow qw(:all);

    my $mocked = mock_object( 'My::Mocked::Package::Name' );
    $mocked->my_method( arg_any(0,9) )->result( 'return value' );

    mock_run();

    die "test did not return right value"
      if $mocked->my_method( 'any', 'arguments' ) ne 'return value';

    mock_end();
    

USAGE

Test::CallFlow functions are used here in a procedural manner because straightforward test scripts are seen as primary use case. As well you may create objects with new() and use the provided functions as object methods.

DECLARING

    use Test::More plan_tests => 1;
    use Test::CallFlow qw(:all);

    # just mock a package
    mock_package( 'Just::Mocked' );

    # mock a package and make an object of it
    my $mocked = mock_object(
        'My::Mocked::Package::Name',          # must specify package name
        { 'optional' => 'content' } );        # may specify what to bless

PLANNING

    Just::Mocked->new()                       # no arguments
                ->result( $mocked );          # return the mock object

    my $get_call =                            # refer to this Test::CallFlow::Call object
        $mocked->get( "FieldX" )              # one equal string argument
             ->result( 1, 2, 3 )              # return array ( 1, 2, 3 ) on first call
             ->result( 4, 5, 6 )              # return array ( 4, 5, 6 ) on second call
             ->result( 7, 8, 9 )              # return array ( 7, 8, 9 ) on any subsequent calls
             ->min(0)                         # this call is optional
             ->max(9)                         # this call can be made at most 9 times
             ->anytime;                       # may be called at this step or any time later

    $mocked->set( arg_check( qr/^Field/ ),    # first argument matching regular expression
                  arg_any( 1, 99 ) );         # 1-99 arguments with any values
                                              # return nothing (undef or empty array)

    $mocked->save( arg_check( \&ok_file ) )   # use own code to check argument
             ->end( $get_call );              # end scope: $get_call can be made no more

    # if you wish to use parts of the real package unmocked as is,
    # load it after planning but before running:
    use My::Mocked::Package::Name;
    
    # remember that nothing keeps you from still just adding your own:
    
    package My::Mocked::Package::Name;
    
    sub really_customized {} # skipping mock system

    package main; # remember to end your own package definition

RUNNING

    mock_run();  # flow of calls from test planned, now prepare to run the test(s)

    eval {

      # package was already declared as loaded at mock_run()
      # so code under test may freely try to 'use' it
      use My::Mocked::Package::Name;

      code_under_test();  # dies on any unplanned call to a mocked package or sub

      mock_end(); # dies if any expected calls were not made and reports them
    };

    is( $@, '', "code_under_test() executed according to prepared plan" );
    
    mock_clear(); # flush state, plan and mocks so you may plan another test call flow

RECORDING

To make it easier to start refactoring existing complicated legacy code, Test::CallFlow also provides preliminary sub call recording functionality:

    # load the packages used by code under test first
    use My::Mocked::Package::Name;
    use Other::Mocked::Package;

    # then declare them for mocking; this saves the original subs aside
    mock_package( 'My::Mocked::Package::Name', 'Other::Mocked::Package' );

    # start recording
    record_calls_from( 'Package::Under::Test' );
    
    # now calls to mocked packages will be made and recorded with their args and results
    use Package::Under::Test;
    Package::Under::Test->code_under_test();
    
    # generate code to serve as basis for your test run
    print join ";\n", map { $_->name() } mock_plan()->list_calls();

OBJECT ORIENTED USAGE

Test::CallFlow is actually object-oriented; default instance creation is hidden. Usability of multiple simultaneous mock objects is hindered by Perl global package namespace. Only one object may be used for recording, planning or running at a time. A separate object can be used for each of those tasks simultaneously as long as they don't mock same packages. Just do one thing at a time and mock_clear() straight after to steer clear of any problems.

  use Test::CallFlow;
  
  my $flow = Test::CallFlow->new(
        autoload_template => '' # do not declare AUTOLOAD, use explicit mock_call()s only
  );

  $flow->mock_package( 'Just::Mocked' );
  $flow->mock_call( 'Just::Mocked::new', 'Just::Mocked' )->result( bless( {}, 'Just::Mocked' ) );
  $flow->mock_run;
  print Just::Mocked->new;
  $flow->mock_end;

PACKAGE PROPERTIES

%Test::CallFlow::state

Map of state names to state IDs. Used to refer to flow object states:

  unknown, record, plan, execute, failed, succeeded.
@Test::CallFlow::state

List of state names. Used to get printable name for state IDs.

%Test::CallFlow::prototype

Contains default values for instance properties.

@Test::CallFlow::instance

Array of created instances. Used by mocked methods to locate the related instance responsible of building and following the plan, ie. checking the call and providing right result to return.

INSTANCE PROPERTIES

Default properties are defined in %Test::CallFlow::prototype. They may be specified as parameters for new or environment variables with prefix mock_, such as mock_save.

Template texts below may contain #{variablename} placeholders that will be replaced by context-specific or Test::CallFlow object property values.

TEMPLATE PROPERTIES

These may be useful for heavier customizations, although it'll probably be easier to just define more hairy mock package parts straight in the test script.

package_template

Template text for mock package definitions. See code for contents.

#{packagename} placeholders will be replaced by name of package to mock.
#{subs} placeholders will be replaced by sub definitions.
sub_template

Template for code to put into mocked subs.

#{packagename} placeholders will be replaced by name of package to mock.
#{subname} placeholders will be replaced by name of sub to mock.
autoload_template

Template for code to put into mocked AUTOLOAD subs.

package_definition_template

Template for package definition at mock_run.

Default value contains redefinition warning suppression and expects #{packagebody} variable to contain actual mock package definition.

INTERNAL PROPERTIES

These are set and used at planning and runtime.

state

One of %Test::CallFlow::state values.

Default is plan. mock_run() sets state to execute. mock_end sets it to succeeded - or failed if more calls were expected. Failure in a mock call sets it to failed. mock_clear and mock_reset unconditionally set it back to plan.

id

Index of this object in @Test::CallFlow::instances.

packages

Contains data about packages and subs to mock gathered from calls in planning mode.

plan

Call execution plan as a Test::CallFlow::Plan object containing Test::CallFlow::Call objects.

record_calls_from

Hash of package names created by record_calls_from() for checking which calls to record during recording.

DEBUGGING PROPERTIES

debug

Controls debug information printing. Class names in this string cause debugging info to be printed from them. Options are: Mock, Plan, Call, ArgCheck. Derived from $ENV{DEBUG}.

debug_mock

Controls whether to print debug info in this class.

PACKAGE SAVING PROPERTIES

Sometimes it might be nice to put the files into a temporary directory included in @INC, or to keep them around for debugging or faster loading later.

save

Whether to save package definitions into files. Default is not to save.

If set at construction, the temporary directory will be prepended to @INC so that the mocks will load with use hiding any real implementations.

basedir

Base directory for saving packages. Default is system temporary directory.

savedir

Template for name of subdirectory inside basedir to contain saved package file hierarchy. Default is 'perl-mock-<process-id>-<mock-instance-number>'.

FUNCTIONS

instance

  $mocker = Test::CallFlow::instance;

Returns the first instance of this class created with given properties. Creates one if there isn't.

This is called from each of the mock_ subs exported with :all tag so that the library can easily be used procedurally.

new

        my $mocker = Test::CallFlow->new( %properties );

Returns a new Test::CallFlow object with given properties. Properties not given are taken from %Test::CallFlow::prototype.

record_calls_from

   record_calls_from( 'Package::Under::Test', 'Supplementary::Package::Under::Same::Test', );

Starts recording calls from specified packages.

Returns self.

mock_run

  mock_run;

End planning mocked calls and start executing tests.

If compilation of a package fails, confesses its whole source.

Returns self.

mock_end

  mock_end;

End test execution.

If any expected calls have not been made, dies with a list of unsatisfied calls.

Returns self.

mock_clear

  mock_clear;

Clears plan. Restores any original subs covered by mocks. Resets state unconditionally back to planning.

Does not touch any other properties of mocked packages than subs mocked with mock_sub() (that's used implicitly during normal planning or recording).

Does not currenctly remove any files created by requesting packages to be saved. Maybe that should some day be a configurable option.

Returns self.

mock_reset

  mock_reset;

Reset mock plan for re-run.

mock_package

  mock_package( 'Package::Name' );

Declares package of given name to be mocked. Returns nothing. Dies if the package declaration fails - ie. when invalid templates were specified for this mock object.

AUTOLOAD method gets declared to enable building plan by mock calls.

mock_object

  my $mocked = mock_object( 'Package::Name' );
  my $mocked_scalar = mock_object( 'Scalar::Blessed', "bless this scalar" );

Returns an object of given mocked package. Declares that package for mocking if necessary.

mock_sub

  my $props_ref = mock_sub( 'Package::Name', 'sub_name', 'sub #{subname} { warn "#{subname}(@_) called" }' );

Declares given package to contain given sub such that it will actually execute Test::CallFlow::mock_call - or alternatively given template text.

Template may contain placeholders marked as #{name} to be substituted with values of any property of the Test::CallFlow object or

subname

Name of sub being defined

packagename

Name of package being defined

mock_call

   mock_call( 'Mocked::Package::sub_name', @args );

Called from mocked packages.

During plan buildup, adds calls to mock call plan list.

During test execution, tries to find a planned mock call matching given call. Returns planned value. Dies on mismatch.

During recording calls the original method. If caller is a record candidate, records the call and result.

mock_plan

Returns reference to the Test::CallFlow::Plan object.

arg_check

  $mocked->method( arg_check(qr/../), arg_check( sub { $_[2]->[$_[1]] < 5 }, 0, 99 ) );

Instantiates an object of correct subclass of Test::CallFlow::ArgCheck for given test; either Regexp or Code reference.

Arguments are

1. The test: a regular expression, code reference or scalar
2. minimum number of arguments to match: 0 for optional
3. maximum number of arguments to match.

arg_any

  $mocked->method( arg_any, 'X', arg_any( 0, -1 ) );

Returns an argument checker that passes any arguments. Optional arguments specify minimum (default 1) and maximum (default same as minimum) possible number of arguments to pass.

INTERNAL METHODS

These are not exported with :all.

save_mock_package

Saves given package if saving is not disabled for it and enabled for it or by default. Location is basedir/savedir/containingpackage/packagename.pm.

Dies on I/O failures.

plan_mock_package

  my $package_definition = plan_mock_package( 'My::Mocked::Package::Name' );

Returns a string containing the perl code for a package with mock versions of all methods called so far.

embed

  my $text = $mocker->embed( 'sub #{subname} { "mocked sub of #{packagename}" }', subname => 'my_mock' );

Embeds given values and object properties as referred by placeholders in given text.

Does not recurse indefinitely, but gives silently up after 15 recursions.

mock_package_filename

  my $filename = mock_package_filename( 'My::Mocked::Package::Name' );

Returns relative path and filename combination string for given package name.

plan_mock_call

  $mocker->plan_mock_call( 'Mocked::Package::sub_name', @args );

Adds a call with given package::sub name and arguments to call plan.

execute_mock_call

Called from mock_call when running tests against plan.

Returns result from planned mock call matching given executed call if one exists.

record_mock_call

Called from mock_call when recording calls.

Returns result of call to original method.

TODO

  • MockCommand

    Integration to cover external command calls.

  • Tied Variables

    Provide easy methods for recording, restricting and testing data access.

  • Test::CallFlow::Package

    Would allow for neat stuff like

      mock_package( 'Bar' )->vars( ISA => [ 'Foo' ], VERSION => 0.01 );
  • ArgCheck::Hash

    ArgChecker for deep structure comparison. Add also arg_deep.

  • ArgCheck::Array

    ArgChecker for a match in a list; used as arg_check( \@in ).

  • Ref Checking

    Document the fact that Regexp /^Type::Name=/ may be used for reference type checks.

AUTHOR

Kalle Hallivuori, <kato at iki.fi>

BUGS

Please report any bugs or feature requests to bug-test-callflow at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-CallFlow. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Test::CallFlow

You can also look for information at:

SEE ALSO

ALTERNATIVES

Test::CallFlow provides a very simple way to plan mocks. Other solutions are available, each with their strong points.

  • Test::MockClass

    Very clearly named methods are used to create and control mocks. Supports explicit call order. Does not provide unified flexible argument checking. Call tracking can be disabled.

  • Test::MockObject

    Collects calls made so that you can check them in your own code afterwards.

  • Test::MockModule

    You provide the code for each mocked method separately. No flow checks. Original methods are remembered and can be restored later.

  • Test::MockCommand

    Mock external commands that your program calls.

SUPPLEMENTARY MODULES

  • Test::CallFlow::Plan

    A structure of calls the code under test should make.

  • Test::CallFlow::Call

    A single call that the code under test might make.

  • Test::CallFlow::ArgCheck

    Checkers for arguments to mocked function calls.

  • Test::CallFlow::ArgCheck::Equals

    Pass arguments that match given string or undef.

  • Test::CallFlow::ArgCheck::Code

    Pass arguments that given method returns true for.

  • Test::CallFlow::ArgCheck::Regexp

    Pass arguments that are defined and match given regexp.

  • Test::CallFlow::ArgCheck::Any

    Pass any arguments.

ACKNOWLEDGEMENTS

  • chromatic, author of Test::MockObject

    Perl namespace management details I got from his code.

  • Simon Flack, author of Test::MockModule

    Perl namespace management details I got from his code.

COPYRIGHT & LICENSE

Copyright 2008 Kalle Hallivuori, all rights reserved.

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