yulivee
yulivee

Reputation: 347

Mocking Git Module that is internally used by another Module

I am trying to Unit-Test my module Importer::Git that uses Git.pm from CPAN and I would like to mock the calls to Git::command,Git::repository and Git::command_oneline etc to not actually alter my filesystem. I tried to do this via Test::MockObject, but it seems I haven't fully understood the internal workings yet...

Example:

package Importer::Git

   sub create_repository {
    my ( $rc, $repo );

    $rc = Git::command_oneline( 'init', $self->targetdir . "/" . $self->name );                                                                                                                                                                                                      

    $repo = Git->repository( Directory => $self->targetdir . "/" . $self->name );
    $rc = $repo->command( 'config', 'user.name', $self->git_import_user->{ name } );

      $self->_repo( $repo );

      return $repo;
   }

Testcase:

use Import::Git;

use Test::More tests => 1;    # last test to print
use Test::Exception;
use Test::MockObject;

# pretend to have loaded the Git Module.
$INC{'Git.pm'} = 1;
my $git = Test::MockObject->new();
$git->fake_module('Git', repository => sub { $git } );
$git->set_true( qw(command command_oneline) );


$repo = Import::Git->init();
$repo->targetdir('./');
$repo->name('testrepo');
$repo->git_import_user({ name => 'test', email => '[email protected]', push_default => 'testpush' });
$repo->create_repository();

But it does not seem to replace the git object in question as this test crashes with the messages from the actual Git.pm Module.

error: Malformed value for push.default: testpush
error: Must be one of nothing, matching, simple, upstream or current.
fatal: bad config variable 'push.default' in file '/home/.../testrepo/.git/config' at line 10
init .//testrepo: command returned error: 128

I guess in those two lines from Importer::Git it doesnt replace $repo

    $repo = Git->repository( Directory => $self->targetdir . "/" . $self->name );
    $rc = $repo->command( 'config', 'user.name', $self->git_import_user->{ name } );

So how would I mock that properly? I would like the $repo->command calls to just return 1.

UPDATE:

Daves guess was right. Correcting the code to this solved it:

use Test::More tests => 1;    # last test to print
use Test::Exception;
use Test::MockObject;

my $git;
BEGIN {
# pretend to have loaded the Git Module.
$DB::single=1;
$INC{'Git.pm'} = 1;
$git = Test::MockObject->new();
$git->fake_module('Git', repository => sub { $git } );
$git->set_true( qw(command command_oneline) );
}

use Import::Git;

 my $real_repo = Import::Git->init();
 $real_repo->targetdir('./');
 $real_repo->name('testrepo');
 $real_repo->git_import_user({ name => 'test', email => '[email protected]', push_default => 'testpush' }); 
 $real_repo->create_repository();

Upvotes: 3

Views: 327

Answers (2)

simbabque
simbabque

Reputation: 54323

An alternative to Dave's excellent answer is to not use the fake_module functionality at all, but instead temporarily overwrite the constructor using something like Sub::Override. I feel that gives more granulated control.

use Test::More;
use Test::MockObject;
use Sub::Override;
use Git (); # we need this so we can override it in case it hasn't been loaded yet

# test code ... 

{
    # this is our faked module
    my $git = Test::MockObject->new;
    $git->set_true( qw(command command_oneline) );

    # we will save the arguments to the constructor here for 
    # inspection in tests later
    my @constructor_args;

    # this will temporarily replace the constructor until $sub
    # goes out of scope
    my $sub = Sub::Override->new(
        'Git::new' => sub {
            @constructor_args = @_; # save args
            return $git;            # return fake
        }
    );

    is something_that_deals_with_git(), $what_you_expect, 'test stuff with git';
    is scalar @constructor_args, 2, '... and Git constructor was called with 2 args';
    # ...
}

It has the drawback that you cannot use Test::MockObject's facilities to look at the calls to new, but there's an easy way to mitigate that with our @constructor_args variable. Everything else stays the same.

Upvotes: 1

Dave Cross
Dave Cross

Reputation: 69244

This is just a guess and I don't have time to test it...

The documentation for the fake_module() method says this:

Beware that this must take place before the actual module has a chance to load. Either wrap it in a BEGIN block before a use or require or place it before a use_ok() or require_ok() call.

I expect that your module (is it called Import::Git or Importer::Git?) loadsGit.pm, so you need to callfake_module()` before loading your module. So something like this, perhaps:

use Test::More tests => 1;    # last test to print
use Test::Exception;
use Test::MockObject;

my $git;

BEGIN {
  # pretend to have loaded the Git Module.
  $INC{'Git.pm'} = 1;
  $git = Test::MockObject->new();
  $git->fake_module('Git', repository => sub { $git } );
  $git->set_true( qw(command command_oneline) );
}

use Import::Git;

Upvotes: 3

Related Questions