Sign In/My Account | View Cart  
advertisement


Listen Print

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

Testing an Actual Method

Once the module is loaded and ready, I like to test my methods in the order in which they appear. This helps to keep the test suite and the module somewhat synchronized. The first method is genObject(). It's fairly simple:


        my $this = shift @_;
        my ($query, $bindNode, $field, $name, $default) =
                getParamArray(
                "query, bindNode, field, name, 
                     default", @_);
        
        $name ||= $field;
        
        my $html = $this->SUPER::genObject(
                $query, $bindNode, $field, $name) . 
                     "\n";
        
        if(ref $bindNode)
        {
                my $author = $DB->getNode($$bindNode{$field});
                if($author && $author->isOfType('user'))
                {
                        $default ||= $$author{title};
                }
                elsif($author)
                {
                        $default ||= "";
                }
        }

        $html .= $query->textfield(-name => $name, 
        	    -default => $default,
                -size => 15, -maxlength => 255,
                -override => 1) . "\n";
                
        return $html;

I can see several spots that need tests. First, I want to make sure that getParamArray() is called with the proper arguments. (This function makes it possible to pass parameters by position or in name => value pair style, similar to Sub::NamedParams.) Next, I'll test that SUPER::genObject() is called correctly, with the proper values. (This call looks odder than it is, due to the way the Engine handles node inheritance. For a good time, read Everything::Node::AUTOLOAD(), or take up bowling.) After that, there's the conditional statement. I'll have to call the function at least three times to test the branches effectively. The function ends with a textfield() call I want to test, and has a return value where I can check some other things. It's not as complex as it looks.

One of the side benefits of testing is that you'll start to write smaller and smaller functions. This is especially evident if you write tests before you write code to pass them. Besides being easier to read and debug, this tends to produce code that's more flexible and much more powerful.

Having identified several things to test, I next write test names. These are short descriptions of the intent of the tests. When confronted with a piece of existing code, I usually try to figure out what kinds of things can break, and what the important behavior really is. With experience, you can look at a piece of well-written code and figure it out intuitively. There's room to be explicit when you're just starting, though.


        # genObject() should call getParamArray()
        # ... passing a string of desired parameters
        # ... and its arguments, minus the object
        # ... should call SUPER::genObject()
        # ... passing the important parameters
        # ... and should call textfield()
        # ... with the important parameters
        # ... returning its and its parents results
        # ... if node is bound, should call getNode()
        # ... on bound node field
        # ... checking for an author node
        # ... and setting the default to the author name
        # ... or nothing, if it is not an author node

That's a pretty good rough draft. Going through the list reveals the need to make at least two more passes through the code. Getting this in the right order takes a little work, but once you know how to set up the mocking conditions correctly, it's really fast and easy. The best way I've found to handle this is just to jump right in.


        can_ok( 'Everything::HTML::FormObject::AuthorMenu', 
        	'genObject' );

First, I want to make sure this method exists. Why? It's part of the ``Do the simplest thing that could possibly work'' principle. Whenever I add a method, I first check to see whether it exists. It sounds too stupid to have any use, but this is a thing that could possibly break. First, I've been known to misspell method names occasionally. This'll catch that immediately. Second, it gives me a place to start and a test that passes with little work. That's a nice psychological boost that moves me on to the next test. I've kept this habit when writing tests for existing code.

Next, I want to test the call to getParamArray(). Since it's not a method, I can't use a mock object. I'll have to mock it sideways. Though the function comes from Everything.pm, it would normally be exported into this package. I'll use a variant of the import()-mockery earlier:


        my ($gpa, @gpa);
        $mock->fake_module( $package,
            getParamArray => sub { push @gpa, \@_; return $gpa }
        );

I can count the elements of @gpa to see whether it was called, and pull the arguments out of the array. $gpa allows me to control the output. The test itself is easy to write:


        my $result = genObject();
        is( @gpa, 1, 'genObject() should call getParamArray' );

OK, it's a little easier to write than it should have been. If you're paying attention, then you should wonder why the genObject() call works, as I'm still in the main package and the method is in the class. I've just added a variable with the tested package name, as well as an AUTOLOAD() function. I'm already tired of typing the big long package name:


        # near the start
        use vars qw( $AUTOLOAD );
        my $package = 'Everything::HTML::FormObject::AuthorMenu';
        
        ...
        
        # way down at the end
        sub AUTOLOAD {
                my ($subname) = $AUTOLOAD =~ /([^:]+)$/;
                if (my $sub = UNIVERSAL::can( $package,
                     $subname )) {
                        $sub->( @_ );
                } else {
                        warn "Cannot call <$subname> 
                             in ($package)\n";
                }
        }

With all of that infrastructure in place, it's a little disappointing to realize that the test dies. Since I'm calling the method as a function, there's no object on which to call SUPER::genObject(). This is easily solved. Remember that $mock (a mock object) was created earlier? Here's one bit of Perl's magic that drives some OO purists crazy, but makes it oh-so-easy to test. A method call is a function call with a special first argument. If $this, inside genObject(), can do everything that an Everything::HTML::FormObject::AuthorMenu object can do, then it'll just work. Hooray for polymorphism!

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

Next Pagearrow