A Test::MockObject Illustrated Example
by chromatic
|
Pages: 1, 2, 3, 4, 5, 6
For the second pass, I will test what happens when I provide a node to which to
bind the form object. In an unmocked Everything, this node comes as an
argument to the function. In the tests, I must return them from the mocked
getParamArray(), so that's where I will start. I also set the 'field' value
in the mock object to a sentinel value I'll check for later. Since the value
of $field will be 'field,' it works out nicely. (There's room to be much
more creative on these names, especially if you're trying to sneak the name of
a friend into your software.)
$gpa{bn} = $mock;
$mock->{field} = 'bound node';
Because getNode() has a series set on it and hasn't been called before, it
will return 0. That means that isOfType() won't be called on the author
object, and the default choice won't be modified from its undefined value.
These tests are fairly easy:
genObject( $mock );
($method, $args) = $mock->next_call();
isnt( $method, 'isOfType',
'... not checking bound node type if it is not found' );
shift @$args;
%args = @$args;
is( $args{-default}, undef, '... and not modifying
default selection' );
As before, next_call() comes in handy. Since I already know that
textarea() will be the first (and last, for this pass) method called, I can
make sure that isOfType() was not called.
Two tests follow. One ensures that the code checks the node's type. The other
makes sure that the default value becomes a blank string if the type is
incorrect. To make this work, I had to modify the existing getNode() series
to return two instances of $mock.
$mock->{title} = 'bound title';
$mock->set_series( 'isOfType', 0, 1 );
genObject( $mock );
($method, $args) = $mock->next_call( 2 );
is( $method, 'isOfType', '... if bound node
is found, should check type' );
is( $args->[1], 'user', '... (the user type)' );
($method, $args) = $mock->next_call();
shift @$args;
%args = @$args;
is( $args{-default}, '',
'... setting default to blank string
if it is not a user' );
The only new idea here is of passing an argument to next_call(). I know
getNode() is the first mocked method, so I can safely skip it. These tests
all pass. The final testable condition is where the bound node exists and
is a 'user' type node. The set_series() call in the last test block makes
isOfType() return true for this pass:
genObject( $mock );
($method, $args) = $mock->next_call( 3 );
shift @$args;
%args = @$args;
is( $args{-default}, 'bound title', '... but using
node title if it is' );
I now have 22 successful tests. My original test name plan had 14 tests, but
that number generally grows as I see more logic branches. I could add more
tests, making sure that default values are not overwritten, and that the
essential (hardcoded) attributes of the textfield() are set, but I'm
reasonably confident in the tests as they stand. If something breaks, then I'll add
a test to catch the bug before I fix it, but what's left is simple enough; I
doubt it will break. (Writing that is a good way to have to eat my words
later.)
Testing Another Method (A Less Detailed Example)
One method remains for this form Object: cgiVerify(). When the Engine
processes input from submitted Form Object forms, it must rebuild the objects.
Then, it checks the input against allowed values for the widgets. This method
does just that. Its code is slightly longer, and reads:
my ($this, $query, $name, $USER) = @_;
my $bindNode = $this->getBindNode($query, $name);
my $author = $query->param($name);
my $result = {};
if($author)
{
my $AUTHOR = $DB->getNode($author, 'user');
if($AUTHOR)
{
# We have an author!! Set the CGI param
# so that the
# inherited cgiUpdate() will just do
# what it needs to!
$query->param($name, $$AUTHOR{node_id});
}
else
{
$$result{failed} = "User '$author'
does not exist!";
}
}
if($bindNode)
{
$$result{node} = $bindNode->getId();
$$result{failed} = "You do not have permission"
unless($bindNode->hasAccess($USER, 'w'));
}
return $result;
Rather than describing the writing of the tests (and my steps and missteps therein), I'll just comment on the tests themselves.
my $qmock = Test::MockObject->new();
$mock->set_series( 'getBindNode', 0, 0, 0,
$mock, $mock );
$qmock->set_series( 'param', 0, 'author', 'author' );
Because of the way this method handles things, it's clearer to create another
mock object to pass in as $query. I'm also setting up the two main series
used for the several passes through this method. While writing the tests, I
kept adding new values to these series. This is what remained at the end.
This approach makes more sense to me than setting each mock before each pass,
but it's a matter of style, and either way will work.
$result = cgiVerify( $mock, $qmock, 'boundname' );
($method, $args) = $mock->next_call();
is( $method, 'getBindNode', 'cgiVerify() should get
bound node' );
is( join(' ', @$args), "$mock $qmock boundname",
'... with query object and query
parameter name' );
Here's the reason I used separate mock objects: to tell them apart as arguments
in the getBindNode() call.

