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

NAME

Class::Mock::Generic::InterfaceTester

DESCRIPTION

A mock object for testing that you call other code correctly

SYNOPSIS

In the code under test:

    package My::Module;

    use Class::Mockable
        _storage_class => 'MyApp::Storage';

and in the tests:

    My::Module->_storage_class(
        Class::Mock::Generic::InterfaceTester->new([
            {
                method => 'fetch',
                input  => [customer_id => 94],
                output => ...
            },
            {
                method => 'update',
                input  => [status => 'fired', reason => 'non-payment'],
                output => 1,
            },
            ...
        ]);
    );

or, more simply:

    my $interface_tester = Class::Mock::Generic::InterfaceTester->new;
    My::Module->_storage_class($interface_tester);
    
    # Expect this method to be called by this test.
    $interface_tester->add_fixtures(
        fetch => {
            input => [customer_id => 94],
            output => ...
        },
    );
    ok(My::Module->something_that_fetches_from_storage(customer_id => 94));

    # Expect these two methods to be called by this next test.
    $interface_tester->add_fixtures(
        update => {
            input  => [status => 'fired', reason => 'non-payment'],
            output => 1,
        },
        uuid => {
            output => 'DEADBEEF-1234-5678-9ABC-1234567890AB',
        }
    );
    ok(My::Module->something_that_updates_storage_for_non_payment);

You can also create mocks by loading them from a file:

    my $interface_tester = Class::Mock::Generic::InterfaceTester->new(
        \"filename.dd"
    );

or:

    my $interface_tester = Class::Mock::Generic::InterfaceTester->new;
    $interface_tester->add_fixtures(
        \"filename.dd"
    );

Yes, that's a reference to a scalar. The scalar is assumed to be a filename which will be read, and whose contents should be valid arguments to create fixtures.

METHODS

new

This is the main method. It creates a very simple object. Pass to it a list or arrayref of fixtures (see add_fixtures for syntax). Any subsequent method calls on that object are handled by AUTOLOAD. Note that because the constructor is Highly Magical you can even provide fixtures for a method called 'new()'. The only ones you can't provide fixtures for are 'AUTOLOAD()' and 'DESTROY()', and possibly add_fixtures.

For each method call, the first element is removed from the array of fixtures. We then compare the name of the method that was called with the name of the method we *expected* to be called. If it's wrong, a test failure is emitted. If that matches, we then compare the actual parameters passed to the method with those in the fixture. If they don't match, then that's a test failure. If they do match, then finally the 'output' specified in the fixture is returned.

Test failures will tell you what the error was, and where the object was created.

If you want to do anything more complicated than compare input exactly, then specify a code-ref thus:

    {
        method => 'update',
        input  => sub { exists({@_}->{fruit}) && {@_}->{fruit} eq 'apple' },
        output => 94
    }

In this case, the actual parameters passed to the method will be passed to that code-ref for validation. It should return true if the params are OK and false otherwise. In the example, it will return true if the hash of args contains a 'fruit' key with value 'apple'.

If you want to do something more complicated than just return a fixed value then specify a reference to a code-ref for the output thus:

    {
        method => 'next_value',
        input  => 94,
        output => \sub { ... }
    }

Note that it must be a reference to a code-ref, to distinguish from the case where you really do want to return a code-ref. The code-ref supplied will be executed and whatever it returns will be returned. If you want to return a reference to a code-ref then you can perpetrate a mess like this:

    output => \sub { \sub { ... } }

add_fixtures

Supplied with either an arrayref or a list of method call fixtures, adds them to the array of fixtures this object maintains internally (although see below for a caveat about this).

At the simplest, a method call fixture is a hashref with keys method, input and output. If you don't care about the input your method receives, you can omit that key and any input will be accepted.

You can also provide a fixture as a pair of method and (hashref containing input and output). This lets you write a series of method call fixtures as an apparent ordered hash, which may feel more natural. As above, you can omit the input field if you don't care. So the following calls are equivalent:

 $interface_tester->add_fixtures(
     [
         {
             method => 'do_something',
             input  => sub { 1 },
             output => 'Yup, done',
         },
         {
             method => 'do_something_with_this',
             input  => ['fish'],
             output => 'Fish cooked',
         }
     ]
 );

 $interface_tester->add_fixtures(
     do_something           => { output => 'Yup, done' },
     do_something_with_this => {
         input  => ['fish'],
         output => 'Fish cooked',
     },
 );

Caveat: just in case you need to test a call to a method that coincidentally is also called add_fixtures, this method is only enabled if you did not provide a list of fixtures to the constructor. Note that this means that you can't use add_fixtures to add a fixture for a method called add_fixtures!

set_name

Takes a scalar parameter and spits that back out at you in any errors, which may make debugging code that used this module easier. This method is only available before you add fixtures. As soon as you add fixtures any calls to set_name are treated as normal mocked method calls.

DESTROY

When the mock object goes out of scope, this is called as usual. It will emit a test failure if not all the fixtures were used.

RECORDING AND PLAYBACK

As noted above you can read fixtures from a file by passing a reference to a scalar filename as the first argument to new or add_fixtures.

Recording fixtures to a file is not yet implemented.

PHILOSOPHY

When you test a piece of code, you want to test it in isolation, because that way when you get test failures it's much easier to find them than if the code you're testing then calls other code, which calls three other modules, which call other modules and so on. If your tests end up running a whole bunch of code other than just the little bit you actually want to test then a failure in any one of those other parts can be very hard to find and fix.

You also want to test all of your code's inputs and outputs. Some inputs and outputs are obvious - the parameters you pass to a method are inputs, and its outputs include the return value and any changes in state that the method call makes. For example, in this accessor:

    package MyApp::SomeModule;

    sub fruit {
        my $self = shift;
        if(@_) { $self->{fruit} = shift; }
        return $self->{fruit};
    }

the inputs are the argument (if supplied), and the outputs are the return value and, if you supplied an argument, the object's changed internal state.

So far, so easy to test.

Now consider a slightly more complex accessor:

    package MyApp::SomeModule;

    sub fruit {
        my $self = shift;
        if(@_) {
            $self->{fruit} = shift;
            $self->log(INFO, "fruit changed to ".$self->{fruit});
        }
        return $self->{fruit};
    }

    sub log {
        my $self = shift;
        my $priority = shift;
        my $message = shift;
        MyApp::Logger->log($priority, $message);
    }

This accessor has an extra output, the call to $self->log(), the method for which is also shown. But when you're testing the accessor, you don't really want the hassle of setting up and configuring logging, nor do you really want to run all the extra code that that entails, all of which is a potential source of confusing test failures and should itself be run in isolation. So, modify the log() method thus:

    package MyApp::SomeModule;

    use Class::Mockable
        _logger => 'MyApp::Logger';

    sub log {
        my $self = shift;
        my $priority = shift;
        my $message = shift;
        $self->_logger()->log($priority, $message);
    }
    

and in the tests ...

    MyApp::SomeModule->_logger(
        Class::Mock::Generic::InterfaceTester->new([
            {
                method => 'log',
                input  => [INFO, "fruit changed to apple"],
                output => "doesn't matter for this test"
            }
        ])
    );

    ...
    ok($object->fruit('apple') eq 'apple',
        "'fruit' accessor returned the right value");
    ok($object->fruit() eq 'apple',
        "... yup, the object's internal state looks like it changed");

That mocks the logger, but still checks that your code called it correctly. The mocking being in the log() method means that the only application code that got run for this test is the fruit() accessor and the log() method - the logger itself wasn't run, it was mocked - so we have proved that all of the fruit() accessor's inputs and outputs, including the method calls that it makes, are correct.

If the log() method call (and hence the call to the mocked logger) is correct, then you shouldn't notice any changes in your tests. But if the accessor's calling of the log() method changes in any way without you also changing the mock (which is effectively a test fixture) then you'll get test failures.

SEE ALSO

Test::MockObject is good for faking up troublesome interfaces to third-party systems - for example, for making a wee pretendy third party web service that the code you're testing wants to talk to. You want to mock such things if the third party service is slow, or unreliable, or not available in all your testing environments. You could also use Class::Mock::Generic::InterfaceTester for this, but often Test::MockObject is simpler. Use Test::MockObject if you care mostly about the data you get back from external code, use Class::Mock::Generic::InterfaceTester if you care more about how you call external code.

AUTHOR

Copyright 2012 - 2020 UK2 Ltd and David Cantrell <david@cantrell.org.uk>

Some contributions from Sam Kington

This software is free-as-in-speech software, and may be used, distributed, and modified under the terms of either the GNU General Public Licence version 2 or the Artistic Licence. It's up to you which one you use. The full text of the licences can be found in the files GPL2.txt and ARTISTIC.txt, respectively.

SOURCE CODE REPOSITORY

<git://github.com/DrHyde/perl-modules-Class-Mockable.git>

BUGS/FEEDBACK

Please report bugs at Github <https://github.com/DrHyde/perl-modules-Class-Mockable/issues>

CONSPIRACY

This software is also free-as-in-mason.