A Test::MockObject Illustrated Example
by chromatic
|
Pages: 1, 2, 3, 4, 5, 6
($method, $args) = $qmock->next_call();
is( $method, 'param', '... fetching parameter' );
is( $args->[1], 'boundname', '... by name' );
isa_ok( $result, 'HASH', '... and should return a data
structure which' );
The weird test name here is another of my little idioms. isa_ok() adds 'isa
(reference type)' to the end of its test names, and I want them to be as clear
as possible when they're displayed.
$mock->set_series( 'getNode', 0, { node_id =>
'node_id' } );
$result = cgiVerify( $mock, $qmock, 'boundname' );
($method, $args) = $mock->next_call( 2 );
is( $method, 'getNode', '... fetching the node, if an
author is found' );
is( join(' ', @$args), "$mock author user",
'... with the author, for the user type' );
is( $result->{failed}, "User 'author' does not exist!",
'... setting a failure message on failure' );
I like the approach of joining the arguments in a string and doing an is()
or a like() call on them. The benefit of like() is that you can ignore
the $self passed as the first argument, because it's the mock object. I
used is() here to make it more explicit what I'm expecting.
$qmock->clear();
$result = cgiVerify( $mock, $qmock, 'boundname' );
($method, $args) = $qmock->next_call( 2 );
is( $method, 'param', '... setting parameters,
on success' );
is( join(' ', @$args), "$qmock boundname node_id",
'... with the name and node id' );
is( $result->{failed}, undef,
'... and no failure message' );
This bit of code gave me trouble until I added the clear() call. It's worth
remembering that a mock object's stack of mocked calls persists through passes.
I had forgotten that, and was using the first param() call instead of the
second. Oops.
Another thing worth noting is that I pass 'undef' as the expected result to
is(). Conveniently, Test::More silently does the right thing.
$mock->set_always( 'getId', 'id' );
$mock->set_series( 'hasAccess', 0, 1 );
$result = cgiVerify( $mock, $qmock,
'boundname', 'user' );
Here, I exercise the method's final clause. These tests are more complex than the code being tested! Sometimes, it works out that way.
$mock->called_pos_ok( -2 , 'getId',
'... should get bound node id, if it exists' );
is( $result->{node}, 'id',
'... setting it in the resulting node field' );
$mock->called_pos_ok( -1, 'hasAccess',
'... checking node access ');
is( $mock->call_args_string( -1, ' ' ),
"$mock user w",
'... for user with write permission' );
I've moved away from the next_call() approach to the older Test::MockObject
behavior of using positions in the call stack. I'm still not quite pleased
with the names of these methods, but the negative subscripts are handy.
(Maybe I need to add prev_call()). All I have to do is remember that
hasAccess() is called last, and that getId() should be called as the
second-to-last method.
The other new method here is call_args_string(), which simply joins the
arguments at the specified call position together. It saves a bit of typing,
most of which is offset by the long method name.
is( $result->{failed}, 'You do not have permission',
'... setting a failure message if user lacks
write permission' );
$result = cgiVerify( $mock, $qmock, 'boundname', 'user' );
is( $result->{failed}, undef, '... and none if the user
has it' );
These final two tests demonstrate how, at the end of a long series of tests, the available options are whittled down. By the final couple of passes, I'm generally testing only one thing at a time. That always seems mathematically poetic, to me, as if I'm refining with Newton's method.
Conclusion
This whole exercise has produced 39 tests. My next step is to update the test plan with this information.
# way back at the top
use Test::More tests => 39;
This makes it easier to see whether too many or too few tests were run. I get
better results about failures and successes this way, too. As it turns out,
writing the tests for cgiVerify() took about 20 minutes, give or take some
laundry-related distractions. That seems about right for 17 tests on code I
haven't read in several months -- about one a minute, when you know what you're
doing.
It's worth noting the features of this module that lead me to consider mock
objects. Mostly, the process of fetching and building nodes is complex enough
that I didn't really want to hook up a fake database, just so I could go
through all of the code paths required to test that an author is really an
author. If the code had gone through some simple mathematical or textual
manipulations, then I would probably have used black-box testing. Code that relies
on a database abstraction layer (as does Everything) generally makes me reach
for Test::MockObject, however.
For more information on what the module can do, please see its documentation. The current stable version is 0.08, though 0.09 will probably have been released by the time this article is stable.

