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

NAME

Test::MethodFixtures - Convenient mocking of externalities by recording and replaying method calls.

SYNOPSIS

Setting up mocked methods in a test script:

    # my-test-script.t
    ...
    use Test::MethodFixtures;

    Test::MethodFixtures->new->mock("Their::Package::Method");

    use My::Package;    # has dependency on Their::Package::Method

    ... # unit tests for My::Package here

Example workflow using the mocked methods:

    $> TEST_MF_MODE=record prove -l my-test-script.t
    $> git add t/.methodfixtures
    $> git commit -m 'methodfixtures for my-test-script.t'

    $> prove -l my-test-script.t   # uses saved data

More configuration options:

    use Test::MethodFixtures
        # optionally specify global arguments
        mode    => 'record',
        storage => ...
        ;

    my $mocker = Test::MethodFixtures->new(
        mode => 'record',    # set locally for this object

        # optionally specify alternative storage:

        # override default storage directory
        dir => '/path/to/storage',

        # use Test::MethodFixtures::Storage::File class (default)
        storage => 'File',

        # use alternative Test::MethodFixtures::Storage object
        storage => $storage_obj,

        # load alternative Test::MethodFixtures::Storage class
        storage => '+Alt::Storage::Class',
        # or:
        storage => { '+Alt::Storage::Class' => \%options },

        # without '+' prefix, 'Test::MethodFixtures::Storage' is prepended to name
    );

    # simple functions and class methods - can store all arguments
    $mocker->mock("Their::Package::Method");

    # object methods - we need to turn $_[0] ($self) into an
    # unique identifier for object, not memory reference
    $mocker->mock( "Their::Object::Method",
        sub { $_[0]->firstname . '-' . $_[0]->lastname } );

    # do the same for other arguments
    $mocker->mock(
        "Their::Object::Method",
        sub {
            (   $_[0],                                       # use as-is
                $_[1]->firstname . '-' . $_[1]->lastname,    # object in $_[1]
                 # no need to list further arguments if no more changes required
            );
        }
    );

    # skipping arguments that shouldn't be saved - set to undef
    $mocker->mock(
        "Their::Package::Method",
        sub {
            (   $_[0],    # keep
                undef,    # discard

                # further args kept as-is
            );
        }
    );

BREAKING CHANGE

v0.07 - the default file storage now uses hyphens instead of colons as package name separators.

DESCRIPTION

Record and playback method arguments, for convenient mocking in tests.

With this module it is possible to easily replace an expensive, external or non-repeatable call, so that there is no need to make that call again during subsequent testing.

This module aims to be low-dependency to minimise disruption with legacy codebases. By default it tries to use Test::MethodFixtures::Storage::File to record method data. Other storage classes can be provided instead, to use modules available to your system.

N.B. This module should be considered ALPHA quality and liable to change.

Despite not providing any test methods, it is under the Test:: namespace to aid discovery and because it makes little sense outside of a test environment. The name is inspired by database 'fixtures'.

Feedback welcome!

ATTRIBUTES

mode

Valid modes are record, playback, auto, and passthrough. See mock().

storage

A Test::MethodFixtures::Storage object.

METHODS

new

    my $mocker = Test::MethodFixtures->new(
        {   mode => 'record',            # override global / ENV
            dir  => '/path/to/storage',  # override default storage directory

            # or use alternative Test::MethodFixtures::Storage object
            storage => $storage_obj,

            # or load alternative Test::MethodFixtures::Storage:: class
            storage => { 'Alt::Storage::Class' => \%options },
        }
    );

Class method. Constructor

mock

    $mocker->mock("Their::Package::method");
    $mocker->mock( "Their::Package::method", sub { ( $_[0], ... ) } );

In record mode mock() stores the return values of the named method against the arguments passed through to generate those return values.

In playback mode mock() retrieves stored return values of the named method for the arguments passed in.

In auto mode mock() either stores or retrieves values, depending on their presence in the saved data. This mode is useful for adding tests without the need to re-record the other calls.

In passthrough mode the arguments and return values are passed to and from the method as normal (i.e. turns off mocking).

The arguments passed to the mocked method are used to create the key to store the results against.

Optionally mock() takes a second argument of a coderef to manipulate @_, for example to prevent storage of a non-consistent value or to stringify an object to a unique identifier.

store

Internal method. Wraps store() on storage object, and adds package versions for this class and the storage class for comparison on retrieval.

retrieve

Internal method. Wraps retrieve() on storage object, and checks package versions are compatible.

BEHAVIOUR

  • Warns if the module versions used to create the saved data is more recent than those currently running.

  • Handles calling context (list or scalar). Satisfies code using wantarray.

RATIONALE

Testing is good, but also hard to do well, especially with complex systems. This module aims to provide a simple way to help isolate code for testing, and get closer to true "unit testing".

Why not mock objects?

Mock objects are a good way to satisfy simple dependencies, but have many drawbacks, especially in complex systems:

  • They require writing of more code (more development time and more chances for bugs). The mocking code may end up being a duplication of existing behaviour of the mocked code.

  • They have to be kept up-to-date with the code that they are mocking, yet are not usually stored with that code or maintained by the same developers. Besides the extra development costs, divergence may only be noticed later and so the tests are of less value.

Further reading

  • https://www.destroyallsoftware.com/blog/2014/test-isolation-is-about-avoiding-mocks

SEE ALSO

SUPPORT

Bugs / Feature Requests

Please report any bugs or feature requests through the issue tracker at https://github.com/mjemmeson/Test-MethodFixtures/issues. You will be notified automatically of any progress on your issue.

Source Code

This is open source software. The code repository is available for public review and contribution under the terms of the license.

https://github.com/mjemmeson/Test-MethodFixtures

    git clone git://github.com/mjemmeson/Test-MethodFixtures.git

AUTHOR

Michael Jemmeson <mjemmeson@cpan.org>

COPYRIGHT

Copyright 2015- Michael Jemmeson

LICENSE

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