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

NAME

Test::MockPackages - Mock external dependencies in tests

VERSION

Version 1.00

SYNOPSIS

 my $m = Test::MockPackages->new();

 # basic mocking
 $m->pkg( 'ACME::Widget' )
   ->mock( 'do_thing' )
   ->expects( $arg1, $arg2 )
   ->returns( $retval );

 # ensure something is never called
 $m->pkg( 'ACME::Widget' )
   ->mock( 'dont_do_other_thing' )
   ->never_called();

 # complex expectation checking
 $m->pkg( 'ACME::Widget' )
   ->mock( 'do_multiple_things' )
   ->is_method()                      # marks do_multiple_things() as a method
   ->expects( $arg1, $arg2 )          # expects & returns for call #1
   ->returns( $retval )
   ->expects( $arg3, $arg4, $arg5 )   # expects & returns for call #2
   ->returns( $retval2 );

 # using the mock() sub.
 my $m = mock({
     'ACME::Widget' => {
         do_thing => [
            expects => [ $arg1, $arg2 ],
            returns => [ $retval ],
         ],
         dont_do_other_thing => [
            never_called => [],
         ],
         do_multiple_things => [
            is_method => [],
            expects => [ $arg1, $arg2 ],
            returns => [ $retval ],
            expects => [ $arg3, $arg4, $arg5 ],
            returns => [ $retval2 ],
         ],
     },
     'ACME::ImprovedWidget' => {
         ...
     },
 });

DESCRIPTION

Test::MockPackages is a package for mocking other packages as well as ensuring those packages are being used correctly.

Say we have a Weather class that can return the current degrees in Fahrenheit. In order to do this it uses another class, Weather::Fetcher which makes an external call. When we want to write a unit test for Weather, we want to mock the functionality of Weather::Fetcher.

Here is the sample code for our Weather class:

 package Weather;
 use Moose;
 use Weather::Fetcher;
 sub degrees_f {
     my ( $self, $zip_code ) = @_;

     my $data = eval { Weather::Fetcher::fetch_weather( $zip_code ) };
     if ( !$data ) {
         return;
     }

     return $data->{temp_f} . "°F";
 }

And here's how we may choose to test this class. In the success subtest, we use the mock() helper subroutine, and in the failure method we use the OOP approach. Both provide identical functionality.

 use Test::More;
 use Test::MockPackages qw(mock);
 subtest 'degrees_f' => sub {
     subtest 'success' => sub {
         my $m = mock({
             'Weather::Fetcher' => {
                 fetch_weather => [
                    expects => [ '14202' ],
                    returns => [ { temp_f => 80 } ],
                 ],
             },
         });

         isa_ok( my $weather = Weather->new, 'Weather' );
         is( $weather->degrees_f( 14202 ), '80°F', 'correct temperature returned' );
     };

     subtest 'failure' => sub {
         my $m = Test::MockPackages->new();
         $m->pkg( 'Weather::Fetcher' )
           ->mock( 'fetch_weather' )
           ->expects( '14202' )
           ->returns();

         my $weather = Weather->new;
         is( $weather->degrees_f( 14202 ), undef, 'no temperature returned' );
     };
 };
 done_testing();

When we run our tests, you can see that Test::MockPackages validates the following for us: 1. the subroutine is called with the correct arguments, 2. the subroutine was called the correct number of times. Lastly, Test::MockPackages allows us to have this mocked subroutine return a consistent value.

         ok 1 - The object isa Weather
         ok 2 - Weather::Fetcher::fetch_weather expects is correct
         ok 3 - correct temperature returned
         ok 4 - Weather::Fetcher::fetch_weather called 1 time
         1..4
     ok 1 - success
         ok 1 - Weather::Fetcher::fetch_weather expects is correct
         ok 2 - no temperature returned
         ok 3 - Weather::Fetcher::fetch_weather called 1 time
         1..3
     ok 2 - failure
     1..2
 ok 1 - degrees_f
 1..1

For more information on how to properly configure your mocks, see Test::MockPackages::Mock.

IMPORTANT NOTE

When the Test::MockPackages object is destroyed, it performs some final verifications. Therefore, it is important that the object is destroyed before done_testing() is called, or before the completion of the script execution. If your tests are contained within a block (e.g. a subtest, do block, etc) you typically don't need to worry about this. If all of your tests are in the top level package or test scope, you may want to undef your object at the end.

Example where we don't have to explicitly destroy our object:

 subtest 'my test' => sub {
     my $m = mock({ ... });

     # do tests
 }; # in this example, $m will be destroyed at the end of the subtest and that's OK.

 done_testing();

Example where we would have to explicitly destroy our object:

 my $m = mock({ ... });
 # do tests
 undef $m;
 done_testing();

CONSTRUCTOR

new( )

Instantiates and returns a new Test::MockPackages object.

You can instantiate multiple Test::MockPackages objects, but it's not recommended you mock the same subroutine/method within the same scope.

 my $m = Test::MockPackages->new();
 $m->pkg('ACME::Widget')->mock('do_thing')->never_called();

 if ( ... ) {
     my $m2 = Test::MockPackages->new();
     $m2->pkg('ACME::Widget')->mock('do_thing')->called(2); # ok
 }

 my $m3 = Test::MockPackages->new();
 $m3->pkg('ACME::Widget')->mock('do_thing')->called(3);        # not ok
 $m3->pkg('ACME::Widget')->mock('do_thing_2')->never_called(); # ok

Both this package, and Test::MockPackages::Package are light-weight packages intended to maintain scope of your mocked subroutines and methods. The bulk of your mocking will take place on Test::MockPackages::Mock objects. See that package for more information.

METHODS

pkg( Str $pkg_name ) : Test::MockPackages::Package

Instantiates a new Test::MockPackages::Package object using for $pkg_name. Repeated calls to this method with the same $pkg_name will return the same object.

Return value: A Test::MockPackages::Package object.

EXPORTED SUBROUTINES

mock( HashRef $configuration ) : Test::MockPackages

mock() is an exportable subroutine (not exported by default) that allows you to quickly configure your mocks in one call. Behind the scenes, it converts your $configuration to standard OOP calls to the Test::MockPackages, Test::MockPackages::Package, and Test::MockPackages::Mock packages.

$configuration expects the following structure:

 {
     $package_name => {
         $sub_or_method_name => [
            $option => [ 'arg1', ... ],
         ],
     }
     ...
 }

$package_name is the name of your package. This is equvalent to the call:

 $m->pkg( $package_name )
 

$sub_or_method_name is the name of the subroutine or method that you'd like to mock. This is equivalent to:

 $m->pkg( $package_name )
   ->mock( $sub_or_method_name )

The value for $sub_or_method_name should be an ArrayRef. This is so we can support having multiple expects and returns.

$option is the name of one of the methods you can call in Test::MockPackages::Mock (e.g. called, never_called, is_method, expects, returns). The value for $option should always be an ArrayRef. This is equivalent to:

 $m->pkg( $package_name )
   ->mock( $sub_or_method_name )
   ->$option( @{ [ 'arg1', ... ] } );

returns_code(&)( CodeRef $coderef ) : Test::MockPackages::Returns

Imported from Test::MockPackages::Returns. See that package for more information.

SEE ALSO

Test::MockPackages::Mock
Test::MockPackages::Returns

AUTHOR

Written by Tom Peters <tpeters at synacor.com>.

COPYRIGHT

Copyright (c) 2016 Synacor, Inc.