Test Driven Development in Perl: Mocking Objects

Previously, I spoke about tests and test driven development in perl, and now I continue with discussion of MockObject and MockModule; why it’s important, and how you should use it.

First, Test::MockObject creates objects with mocked class names and subroutines on the fly, skipping the normal ‘use’ and ‘bless’ processes.

This is very important when doing testing, as you don’t want the fact that a database is unavailable, or a class is not fully implemented, to break your tests. You want to have your class working regardless of the work of others, as you’ll certainly be working in a group of others while developing at some point (or, you should at least hope to).

Test::MockObject

In order to demo mock object, I’m going to pretend I’m writing a Util class for building URI’s for members of a website. The URI’s will consist of a constant ‘/u’ to start with, then the ‘/{id}’, followed by a normalized user name, where all white space and special characters are replaced with hyphens, and the name is lower cased. It is a little more complex than that, as you’ll see in the test cases, where I’ll make my first contract as to what I’m going to develop.

So we’re going to assume ‘normalize_member_name’ will do all of the specifics about the member name, while ‘normalize_member’ will receive a ‘member’ object. This member object could come in the form of a DBIx result, or some kind of custom object schema wrapper for a document store, etc. What is important is that you expect it to work in a certain way, and that contract is in place. For instance, you will know that the member name will be in the attribute ‘name’, and the user id will be in the attribute ‘id’. Normalize_member will make calls to these to build the uri. Since this object class is not yet defined, we will Mock an object that will act like the object we expect so that we can write our test cases before that piece of code is done, or so that it will pass even when our database is unavailable.

Here is our test case with MockObject in place of a member object, written int t/00_build_uri.t:

#! /usr/bin/perl -w

use Test::More tests => 9;
use Test::MockObject;

BEGIN {
        use_ok('Util::BuildURI');
}

TODO: {
        local $todo = "These items are not yet implemented";
        is( Util::BuildURI::normalize_member_name('Paul Salcido') , 'paul-salcido' );
        is( Util::BuildURI::normalize_member_name('Paul  Salcido') , 'paul-salcido' );
        is( Util::BuildURI::normalize_member_name('  Paul  Salcido') , 'paul-salcido' );
        is( Util::BuildURI::normalize_member_name('  Paul  Salcido   ') , 'paul-salcido' );
        is( Util::BuildURI::normalize_member_name('  Paul @@ Salcido   ') , 'paul-salcido' );
        is( Util::BuildURI::normalize_member_name('  Paul @@ Salcido   Jr.') , 'paul-salcido-jr' );
        is( Util::BuildURI::normalize_member_name('  Paul @@ Salcido 123 Jr.') , 'paul-salcido-123-jr' );

        my $member = Test::MockObject->new();
        $member->mock('name', sub { return 'Paul Salcido' });
        $member->mock('id' , sub { return 1 });
        is( Util::BuildURI::normalize_member({ member => $member }) , '/u/1/paul-salcido' );
}

I haven’t even started coding yet. Here is my prove output:

psalcido@psalcido-ubuntu:~/Documents/devel/test/test-mock$ prove
t/00_build_uri.t .. 1/9 
#   Failed test 'use Util::BuildURI;'
#   at t/00_build_uri.t line 7.
#     Tried to use 'Util::BuildURI'.
#     Error:  Can't locate Util/BuildURI.pm in @INC (@INC contains: /etc/perl /usr/local/lib/perl/5.14.2 /usr/local/share/perl/5.14.2 /usr/lib/perl5 /usr/share/perl5 /usr/lib/perl/5.14 /usr/share/perl/5.14 /usr/local/lib/site_perl .) at (eval 6) line 2.
# BEGIN failed--compilation aborted at (eval 6) line 2.
Undefined subroutine &Util::BuildURI::normalize_member_name called at t/00_build_uri.t line 12.
# Looks like you planned 9 tests but ran 1.
# Looks like you failed 1 test of 1 run.
# Looks like your test exited with 255 just after 1.
t/00_build_uri.t .. Dubious, test returned 255 (wstat 65280, 0xff00)
Failed 9/9 subtests 

Test Summary Report
-------------------
t/00_build_uri.t (Wstat: 65280 Tests: 1 Failed: 1)
  Failed test:  1
  Non-zero exit status: 255
  Parse errors: Bad plan.  You planned 9 tests but ran 1.
Files=1, Tests=1,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.02 cusr  0.00 csys =  0.04 CPU)
Result: FAIL

Now I begin coding Util::BuildURI, with the following:

package Util::BuildURI;

sub normalize_member { 
        my $p = shift;
        $member = $p->{member};
        return '/u/' . $member->id . '/'
                . normalize_member_name($member->name);
}

sub normalize_member_name {
        my $name = shift;
        return $name;
}

1;

Running prove now produces much better output:

psalcido@psalcido-ubuntu:~/Documents/devel/test/test-mock$ prove
t/00_build_uri.t .. 1/9 
#   Failed test at t/00_build_uri.t line 12.
#          got: 'Paul Salcido'
#     expected: 'paul-salcido'

#   Failed test at t/00_build_uri.t line 13.
#          got: 'Paul  Salcido'
#     expected: 'paul-salcido'

#   Failed test at t/00_build_uri.t line 14.
#          got: '  Paul  Salcido'
#     expected: 'paul-salcido'

#   Failed test at t/00_build_uri.t line 15.
#          got: '  Paul  Salcido   '
#     expected: 'paul-salcido'

#   Failed test at t/00_build_uri.t line 16.
#          got: '  Paul @@ Salcido   '
#     expected: 'paul-salcido'

#   Failed test at t/00_build_uri.t line 17.
#          got: '  Paul @@ Salcido   Jr.'
#     expected: 'paul-salcido-jr'

#   Failed test at t/00_build_uri.t line 18.
#          got: '  Paul @@ Salcido 123 Jr.'
#     expected: 'paul-salcido-123-jr'

#   Failed test at t/00_build_uri.t line 23.
#          got: '/u/1/Paul Salcido'
#     expected: '/u/1/paul-salcido'
# Looks like you failed 8 tests of 9.
t/00_build_uri.t .. Dubious, test returned 8 (wstat 2048, 0x800)
Failed 8/9 subtests 

Test Summary Report
-------------------
t/00_build_uri.t (Wstat: 2048 Tests: 9 Failed: 8)
  Failed tests:  2-9
  Non-zero exit status: 8
Files=1, Tests=9,  0 wallclock secs ( 0.03 usr  0.00 sys +  0.02 cusr  0.00 csys =  0.05 CPU)
Result: FAIL

Now I can program my code, and I don’t need to write any custom scripts to test various outputs, nor do I need to write any command line scripts. I’ve also made a contract with others as to how my code will eventually work, and they can mock before they see my utilities at all. When I finally finish programming the Util module:

package Util::BuildURI;

sub normalize_member { 
        my $p = shift;
        $member = $p->{member};
        return '/u/' . $member->id . '/'
                . normalize_member_name($member->name);
}

sub normalize_member_name {
        my $name = lc shift;
        $name =~ s/^[^\w\d]*(.*[\w\d])[^\w\d]*$/$1/;
        $name =~ s/[^\w\d]+/-/g;
        return $name;
}

1;

I now get a pass on all tests:

psalcido@psalcido-ubuntu:~/Documents/devel/test/test-mock$ prove
t/00_build_uri.t .. ok   
All tests successful.
Files=1, Tests=9,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.02 cusr  0.00 csys =  0.04 CPU)
Result: PASS

Test::MockModule

Now we are going to write a basic member object using Moose, and add a subroutine for it that will return the normalized uri from Test::BuildURI. We are going to write this so that we don’t expect Test::BuildURI to be completed yet (even though I’ve done the work). I’m going to intentionally write this ‘broken’, so that we can see this at work.

#! /usr/bin/perl -w

use Test::More tests => 2;
use Test::MockModule;

BEGIN {
        use_ok('MyApp::Member');
}

{
        # Localize the module mocking, it will return to normal out of this scope.

        my $module = new Test::MockModule('Util::BuildURI');
        $module->mock( 'normalize_member' , sub { '/u/2/paul-salcido' } );
        
        my $obj = MyApp::Member->new({ 
                name => 'Paul Salcido Testing',
                id => 2,
        });
        
        is ( $obj->normalized_uri , '/u/2/paul-salcido' );
}

Note that the name that I am giving my member is ‘Paul Salcido Testing’, but I have intentionally broken ‘normalize_member’ of Test::MockModule to always return ‘/u/2/paul-salcido’, instead of a correct ‘/u/2/paul-salcido-testing’. I go ahead and write up a module to handle this:

package MyApp::Member;

use Moose;
use Util::BuildURI;

has 'name' => ( is => 'ro' , isa => 'Str' );
has 'id' => ( is => 'ro' , isa => 'Str' );

sub normalized_uri {
        my $self = shift;
        return Util::BuildURI::normalize_member({ member => $self });
}

1;

While this is a crappy test, you should get the picture. All that I want to make sure of is that the normalized_uri of the Member class returns the value from as normalize_member. Now when I run my tests:

psalcido@psalcido-ubuntu:~/Documents/devel/test/test-mock$ prove
t/00_build_uri.t ......... ok   
t/01_member_normalize.t .. ok   
All tests successful.
Files=2, Tests=11,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.20 cusr  0.00 csys =  0.22 CPU)
Result: PASS

Unit Testing using MockObject and MockModule

While this was an overly simplistic example of Test::MockObject and Test::MockModule for most cases, developing a full skill set for unit testing without major dependencies is going to help you in a major way. When writing your unit tests, you are not going to want to see 50 different files fail when all of your problems can be tied back to one module. You want to see only one fail – that module. With the code that I have above, I do need to have Util::BuildURI existing as a module file somewhere, but if I remove the subroutine normalize_member in its entirety, I still get a passing test for MyApp::Member:

psalcido@psalcido-ubuntu:~/Documents/devel/test/test-mock$ prove
t/00_build_uri.t ......... 1/9 Undefined subroutine &Util::BuildURI::normalize_member_name called at t/00_build_uri.t line 12.
# Looks like you planned 9 tests but ran 1.
# Looks like your test exited with 255 just after 1.
t/00_build_uri.t ......... Dubious, test returned 255 (wstat 65280, 0xff00)
Failed 8/9 subtests 
t/01_member_normalize.t .. ok   

Test Summary Report
-------------------
t/00_build_uri.t       (Wstat: 65280 Tests: 1 Failed: 0)
  Non-zero exit status: 255
  Parse errors: Bad plan.  You planned 9 tests but ran 1.
Files=2, Tests=3,  0 wallclock secs ( 0.03 usr  0.01 sys +  0.20 cusr  0.00 csys =  0.24 CPU)
Result: FAIL

Now I know exactly where the problem is with my code base, and I can go back and finish that development, or work on other pieces of code to get tests to pass, without having to worry about whether or not that chunk of code is done or not. This is now an optimal situation, with the caveat that if I could fully mock the object before use and not have to worry about the file existing, then I would be doing superbly awesome. There might be a way, and I’ve just not learned it yet, but this is better than nothing.

Too Many Mocks

Checking your mocks and how many you have to test one small piece of code is a good way to see if you are engineering your subroutines poorly. If too much mocking is occurring when you are writing a test case, it is a good opportunity to split up the code, and tests, into different subroutines. This is a classic code ‘smell’ that you hear about every so often.

Conclusion

You should take advantage of these tools in order to Unit Test your application, because it allows you to create what I call a ‘contract’ before you even start developing. With all of your tests in place, people that you are developing with will also be able to see what your object or module will eventually look like. With that in mind, in the above example, we should have expanded on our MyApp::Member class in the test cases. Also, when we are testing the object against the data pulls from what might be our datastore, we can override the ‘find’ or ‘select’ clauses (however it is developed), so that we can validate that our object builds properly with data that we would expect from the datastore, while not actually having a live datastore.

All of this improves the ability of a team to work together, to engineer a proper solution, and to guarantee that the code is working properly. I’d even bet that it would increase the speed of development for an organization.

Of course, the limit to Unit testing is that we haven’t touched integration yet. Also to be considered is Test::Continuous and a few other items of note in perl testing, and into the nitty gritty of TAP::Harness, and new things that can be found in perl testing.

Advertisements

One comment

  1. Thanks for another magnificent article. Where else
    may just anybody get that kind of information in such a perfect approach of writing?
    I have a presentation next week, and I’m at the look for such information.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: