Sign In/My Account | View Cart  
advertisement


Listen Print

A Test::MockObject Illustrated Example
by chromatic | Pages: 1, 2, 3, 4, 5, 6

To make the SUPER::genObject() call pass, that call will also have to be mocked. The method resolves to Everything::HTML::FormObject::genObject(), so I'll add a function of the appropriate name and package. (This test code is starting to look familiar. Again, Test::MockModule, anyone?)


        my @go;
        $mock->fake_module( 'Everything::HTML::FormObject',
            genObject => sub { push @go, \@_; 
                 return 'some html' }
        );

Now I modify the genObject() call, passing in my mock object:


        my $result = genObject( $mock );

I get further before things fail. Since there's nothing passed in for the $query variable to hold, the textfield() call fails. Now I can finally use my mock object to good effect. First, I'm going to change what getParamArray() returns, using $package again to save on typing:


        my (%gpa, @gpa);
        $mock->fake_module( $package,
            getParamArray => sub { push @gpa, \@_; return 
            @gpa{qw( q bn f n d )}}
        );

Since AuthorMenu expects to receive its arguments in order, I'll create a hash where I can store them. I might use more descriptive key names, but they seem to make sense now. Next, I'll make sure that 'q' returns something controllable. In this case, that means that it supports the textfield() method and returns something sane:


        $mock->set_always( 'textfield', 'more html' );
        $gpa{q} = $mock;

I could create a new mock object for this, but since there's no name collision yet, it's not a big priority. Whether you do this is a matter of personal style.

For now, genObject() does not die, and all tests pass. Whew. Next up, I test to see whether the first argument to getParamArray() is correct.


        is( $gpa[0][0], 'query, bindNode, field, name, default',
            '... requesting the appropriate arguments' );

It is, so I'll make sure that it passes along the rest of the method arguments, minus the object itself:


        like( join(' ', @{ $gpa[0] }), qr/1 2 3$/,
                '... with the method arguments' );
        unlike( join(' ', @{ $gpa[0] }), qr/$mock/,
                '... but not the object itself' );

Only the first of these fails, and that's because I'm not passing any other arguments to the method call. I'll modify it:


        my $result = genObject( $mock, 1, 2, 3 );

This gives me nine tests that pass. I'm also following the test names fairly closely. (In between writing those and actually writing this code, a couple of days passed. Their similarities make me think I'm on the right track.)

Since the next piece of the code tries to load a bound node and I'm not passing one in, I'll test to see that getNode() is not called. Since the call is on the $DB object, I'll set it to the mock object. I'll also use the called() method to make sure that nothing happens. For that to happen, I need to mock getNode(). The code to implement all of this is pretty simple. (Note that it must go in various places):


        $Everything::HTML::FormObject::AuthorMenu::DB = $mock;
        $mock->set_series( 'getNode', 0, 1, 2 );

        # genObject() calls skipped in this example...

        ok( ! $mock->called( 'getNode' ),
                '... should not fetch bound node without one' );

Two things bear more explanation. First, since I don't really know what I want getNode() to return, I'll give it a dummy series. (I'm pretty sure I'll be using set_series() on it, because I've done tests like this before. I can't explain it much beyond intuitive experience.) Second, I'm negating the return value of called() so it will fit with ok(). This can be a little hard to see, sometimes.

The last few tests of the first pass all revolve around the textfield() call. I've already mocked it, so now I'll see whether it was called with the correct arguments:


        my ($method, $args) = $mock->next_call();
        shift @$args;
        my %args = @$args;

        is( $method, 'textfield', '... and should create 
             a text field' );
        is( join(' ', sort keys %args ),
        join(' ', sort qw( -name -default -size 
             -maxlength -override )), '... passing the 
             essential arguments' );

Several bits here stick out. The next_call() method iterates through the call stack of mocked methods, in order. It doesn't track every method called, just the ones you've added through one of Test::MockObject's helper methods. next_call() returns the name of the method (in scalar context) or the name of the method an an anonymous array containing the arguments (in list context).

Since I want to check the arguments passed to textfield(), I call it in list context. Because the arguments are passed as key => value pairs, the most natural comparison seems to be as a hash. I use the join-sort idiom quite often, as I've never quite been comfortable with the list comparison functions of Test::More. This test would probably be much simpler if I used them.

I explicitly sort both arrays just so a hardcoded list order won't cause unnecessary test failures. (This has bitten me when writing code that ought to work on EBCDIC machines, not just ASCII ones. Of course, if you get Everything up and going on a mainframe, then this is probably the least of your concerns.)

Finally, I test to see whether the returned data is created properly:


        is( $result, "some html\nmore html\n",
            '... returning the parent object plus the 
             new textfield html' );

So far, all 13 of the tests succeed. At this point, I started my second pass through the method, but noticed that I hadn't yet tested that $name would get its default value from $field. I'll add 'field' to %gpa before the first pass:


        $gpa{f} = 'field';

This test ought to go before the final test in this pass, so I add it there, too. This finishes the first pass:


        is( $args{-name}, 'field',
                '... and widget name should default 
                to field name' );

Pages: 1, 2, 3, 4, 5, 6

Next Pagearrow