When you're writing tests for your new module, one of the hardest things is creating the environment that it needs to run in. Often you'll find that your object will rely on a database connection, or a vast object structure that you can't easily create in the test suite.
Other times you'll find that your class's job is to interact with another object which will change it's internal state in some way - and that it's really complex to test without understanding how the other object works - not fun if you haven't written it yourself. And worse - if anyone ever changes the way the internals of that object works then your test suite will break.
It's all really messy. That's where Test::MockObject comes in. It allows you to quickly declare objects that have methods. These can be set up to return various results when they're called, and you can query the mocked object to see what methods have been called, allowing it to be used in both the above situations.
Okay, it's time for another silly Buffy based example.
Imagine we have a object named Doyle. This object listens on a
socket across the Internet for messages from another server call TPTB
(short for 'The Powers That Be'.) When it receives messages it
creates instances of the Monster class and stores them. There's
another class called "Angel" that is responsible for clearing up these
monsters whenever something calls it's need_help
method by
calling Doyle's get_monster
method and dealing with the results
one by one.
Though much toil and trouble, pretend we've managed to fully test our Doyle object and we're sure it's working. And we're sure that the Monsters class is working fine too. It's the Angel class we're worried about - we've never written tests for it and sometimes it has a nasty habit of going bad.
First up, let's look at the code for Angel, so we know what we're talking about:
package Angel;
# turn on Perl's safety features use strict; use warnings;
# the constructor sub new { my $class = shift; my $self = bless {}, $class; $self->{fought} = []; return $self; }
# accessor method people can use to set the helper classes # that we use to get our monsters sub set_doyle { my $self = shift; $self->{doyle} = shift; }
sub set_cordy { my $self = shift; $self->{cordy} = shift; }
# main method, calls doyle/cordy and asks about monsters # and then deals with them sub need_help { my $self = shift;
# find out where the monsters are my @monsters; if ($self->{doyle}->alive) { @monsters = $self->{doyle}->get_monsters(); } else { @monsters = $self->{cordy}->get_monsters(); }
# deal with each monster in turn foreach my $monster (@monsters) { # record it as a monster we've fought push @{$self->{fought}}, $monster;
# kill it if ($monster->type eq 'vampire') { $monster->stake; } else { $monster->stab; } } }
# return a list of things we fought sub fought { my $self = shift; return @{$self->{fought}} }
1;
So we start the test script for the Angel class like so
#!/usr/bin/perl
# we're running tests, though we don't know how many # just yet change this when we know how many use Test::More qw(no_plan);
# turn on Perl's safety features use strict; use warnings;
# test that the Angel class compiles BEGIN { use_ok "Angel"; };
# check that if we call the constructor we get a # proper object back again my $angel = Angel->new(); isa_ok($angel,"Angel");
Now, we can't just create a Doyle object as that requires a live connection to the TPTB server, and we'd have to convince that to generate Monster events when we're testing and we can't determine when that's going to happen. So what we do is we fake it.
# create a fake doyle, and tell Angel where to find it use Test::MockObject; my $doyle = Test::MockObject->new(); $angel->set_doyle($doyle);
Now we need to create responses. Since the system is designed to be
able to function even if we our Doyle fails, our real Doyle
class has an alive
method that returns true if and only if it is
working and has a connection to TPTB. Since this method is called
by our Angel code our fake mocked version needs to have a method
like that too.
# add a 'alive' method to the doyle object that # always returns true. $doyle->set_true('alive')
So now whenever we call
$doyle->alive();
it'll return true, so our Angel code will run okay. Of course later on when we want to test the fail over we'll probably do something like.
$doyle->set_false('alive');
We can also easily mock other methods to always return whatever we want, for example:
$doyle->set_always('unique_object_id', 'Allen Francis Doyle');
So, back to the tests. We simply want our mock object to return some
new monsters each time its get_monsters
method is called by the
Angel object. To do that we can use the mock
method that allows
us to specify a code ref that is called every time an attempt to call
a particular method is made.
# every time 'get_monsters' is called return two new monsters use Monster; $doyle->mock('get_monsters', sub { return (Monster->new(), Monster->new()) });
# call the need_help method a number of times $angel->need_help; $angel->need_help; $angel->need_help;
# angel should have fought 3 x 2 monsters by now is($angel->fought, 6, "fought right number of monsters");
So we've satisfied ourselves that the Angel object is indeed getting monsters back from the Doyle object. However, we haven't checked if the Angel object is attempting to kill the monsters properly.
This is an example of the problem I alluded to in the foreword...how can we actually tell if a method has been called on an object used by our class or not? The answer is simple, we mock that object as well since Test::MockObject objects have the exceedingly handy feature of being able to keep track of how many times each of their methods have been called.
# create a mock monster, which is a vampire and has stake # and stab methods my $monster = Test::MockObject->new(); $monster->set_always(type => 'vampire'); $monster->set_always(stake => 'foo'); $monster->set_always(stab => 'foo');
# create a second mock monster, which isn't a vampire and # has stake and stab methods my $monster2 = Test::MockObject->new(); $monster2->set_always(type => 'demon'); $monster2->set_always(stake => 'foo'); $monster2->set_always(stab => 'foo');
# reconfigure doyle to return $monster the first time # get_monsters is called and $monster2 the second $doyle->set_series('get_monsters', $monster, $monster2);
Now when we call need_help
on the Angel object it should
get a vampire the first time and a demon the second, and call the
stake method for the former and the stab for the latter.
# deal with both monsters $angel->need_help; $angel->need_help;
ok($monster->called('stake'), "vampire staked"); ok(!$monster->called('stab'), "vampire not stabbed"); ok(!$monster2->called('stake'), "demon not staked"); ok($monster2->called('stab'), "demon stabbed");
In addition to the techniques I've described here, Test::MockObject provides many other useful methods. It has other more flexible ways to set what methods return, and many more interesting ways to query exactly what methods have been called, all of which are documented very well in the perldoc for the module.