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!

