Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

Perl Code Kata: Mocking Objects
by Stevan Little | Pages: 1, 2

The Solution

I designed each use case to illustrate a different capability of Test::MockObject.

  • User does not have Geo::IP installed.

    use Test::More tests => 4;
    use Test::MockObject;
    
    my $mock = Test::MockObject->new();
    $mock->fake_module('Geo::IP' => (
        'import' => sub { die "Could not load Geo::IP" },
    ));
    
    use_ok('Site::Member');
    
    my $u = Site::Member->new();
    isa_ok($u, 'Site::Member');
    
    my $warning;
    local $SIG{__WARN__} = sub { $warning = shift };
    
    ok(!defined($u->city()), '... this should return undef');
    like($warning, 
            qr/^You must have Geo\:\:IP installed for this feature/, 
            '... and we should have our warning');

    This use case illustrates the use of Test::MockObject to mock the failure of the loading of an optional resource, which in this case is the Geo::IP module.

    The sample code attempts to load Geo::IP by calling eval "use Geo::IP". Because use always calls a module's import method, it is possible to exploit this and mock a Geo::IP load failure. This is easy to accomplish by using the fake_module method and making the import method die. This then triggers the warning code in the city method, which the $SIG{__WARN__} handler captures into $warning for a later test.

    This is an example of a failure edge case which would be difficult to test without Test::MockObject because it requires control of the Perl libraries installed. Testing this without Test::MockObject would require altering the @INC in subtle ways or mocking a Geo::IP package of your own. Test::MockObject does that for you, so why bother to re-invent a wheel if you don't need to?

  • User has Geo::IP installed but does not have the city data.

    use Test::More tests => 3;
    use Test::Exception;
    use Test::MockObject;
    
    my $mock = Test::MockObject->new();
    $mock->fake_module('Geo::IP' => (
        'open'           => sub { undef },
        'GEOIP_STANDARD' => sub { 0 }
    ));
    
    use_ok('Site::Member');
    
    my $u = Site::Member->new();
    isa_ok($u, 'Site::Member');
    
    $u->ip_address('64.40.146.219');
    
    throws_ok {
        $u->city()
    } qr/Could not create a Geo\:\:IP object/, '... got the error we expected';

    This next use case illustrates the use of Test::MockObject to mock a dependency relationship, in particular the failure case where Geo::IP cannot find the specified database file.

    Geo::IP follows the common Perl idiom of returning undef if the object constructor fails. The example code tests for this case and throws an exception if it comes up. Testing for this failure uses the fake_module method again to hijack Geo::IP and install a mocked version of its open method (the code also fakes the GEOIP_STANDARD constant here). The mocked open simply returns undef which will create the proper conditions to trigger the exception in the example code. The exception is then caught using the throws_ok method of the Test::Exception module.

    This example illustrates that it is still possible to mock objects even if your code is not in the position to pass in a mocked instance itself. Again, to test this without using Test::MockObject would require control of the outside environment (the Geo::IP database file), or in some way having control over where Geo::IP looks for the database file. While well-written and well-architected code would probably allow you to alter the database file path and therefore test this without using mock objects, the mock object version makes no such assumptions and therefore works the same in either case.

  • User has Geo::IP and the Geo-IP city data installed correctly.

    use Test::More tests => 7;
    use Test::MockObject;
    
    my $mock = Test::MockObject->new();
    $mock->fake_module('Geo::IP' => (
        'open'           => sub { $mock },
        'GEOIP_STANDARD' => sub { 0 }
    ));
    
    my $mock_record = Test::MockObject->new();
    $mock_record->set_always('city', 'New York City');
    
    $mock->set_always('record_by_addr', $mock_record);
    
    use_ok('Site::Member');
    
    my $u = Site::Member->new();
    isa_ok($u, 'Site::Member');
    
    $u->ip_address('64.40.146.219');
    
    is($u->city(), 'New York City', '... got the right city');
    
    cmp_ok($mock->call_pos('record_by_addr'), '==', 0,
            '... our mock object was called');
    is_deeply(
            [ $mock->call_args(0) ],
            [ $mock, '64.40.146.219' ],
            '... our mock was called with the right args');
            
    cmp_ok($mock_record->call_pos('city'), '==', 0,
            '... our mock record object was called');
    is_deeply(
            [ $mock_record->call_args(0) ],
            [ $mock_record ],
            '... our mock record was called with the right args');

    This next case illustrates a success case, where Geo::IP finds the database file it wants and returns the expected results.

    Once again, the fake_module method of Test::MockObject mocks Geo::IP's open method, this time returning the $mock instance itself. The code creates another mock object, this time for the Geo::IP::Record instance which Geo::IP's record_by_addr returns. Test::MockObject's set_always method mocks the city method for the $mock_record instance. After this, Geo::IP's record_by_addr is mocked to return the $mock_record instance. With all of these mocks in place, the tests then run. After that, inspecting the mock objects ensures that the code called the correct methods on the mocked objects in the correct order and with the correct arguments.

    This example illustrates testing success without needing to worry about the existence of an outside dependency. Test::MockObject supports taking this test one step further and providing methods for inspecting the details of the interaction between the example code and that of the mocked Geo::IP module. Accomplishing this test without Test::MockObject would be almost impossible given the lack of control over the Geo::IP module and its internals.

Conclusion

Mock objects can seem complex and overly abstract at first, but once grasped they can be a simple, clean way to make hard things easy. I hope to have shown how creating simple and minimal mock object with Test::MockObject can help in testing cases which might be difficult using more traditional means.