Quick and simple mock objects with MooseX::Declare

Sat Oct 10 18:20:00 2009

Digg! submit to reddit Delicious RSS Feed Readers

Once I used mock objects with Test::MockObject but I was never entirely happy with the interface, and it always drove me insane that it forcibly loaded UNIVERSAL::isa and UNIVERSAL::can, which could make tests that should fail when you're doing proxy/decorator object trickery pass instead.

Then, I used Data::Thunk to make them, like this:

  use Data:Thunk qw(lazy_object);

  sub mock {
    my $class = shift;
    lazy_object { confess "Mock ${class} evaluated" }
      class => $class,
      DESTROY => sub {},
      @_;
  }

  my $send;

  my $s = mock 'MailerForm::MailSender' => send => sub { goto &$send };

And that worked pretty well, although occasionally Data::Thunk decided to explode and nothingmuch came to the conclusion that it would be hard to fix, and I've not yet been bored enough to find out if he's wrong.

Now, I have a much simpler solution, courtesy of Devel::Declare. Recently I found myself needing to mock both a factory object and its resultant object. The BuildEngine class in this codebase has a build_sequence_for method that takes some arguments and constructs a BuildSequence object, and I wanted to stub both out. So, I wrote:

  class Test_BuildEngine {
  
    has 'cb' => (is => 'ro');
  
    method build_sequence_for($pv) {
      Test_BuildSequence->new(pv => $pv, cb => $self->cb);
    }
  
    sub isa {
      return 1 if ($_[1] eq 'Shadowcat::Crucible::BuildEngine');
      return shift->SUPER::isa(@_);
    }
  }
  
  class Test_BuildSequence {
  
    has [ 'pv', 'cb' ] => (is => 'ro');
  
    method run (Any $data) {
      $self->${\$self->cb}($self->pv, $data);
    }
  }

using the wonderful MooseX::Declare to create a couple classes on the fly. The overidden 'sub isa' handles reporting that we're a build engine even though we're an independent class, and the callback is passed down to the build sequence object and invoked when its ->run method is called (see my post Madness with Methods for an explanation of the way it's called).

This means we can then write code like the following:

  my $be = Test_BuildEngine->new(
    cb => sub {
            my ($self, $pv) = @_;
            die "build sequence called twice!" if ($info{bs_cb}||=0)++;
            $info{pv} = $pv;
            push(@{$depot->builds}, mk_build($pv));
            return SUCCESS;
          }
  );

which fakes out the run() methods operation, provides a suitable return value, and records what happened so that I can run tests on it afterwards (I prefer this approach to testing in-line in the mock since it means you can run a full method and then analyze the data before writing tests).

Notice in this and the previous approach we had a coderef for the "body" of the mocked method so that it can be replaced over the course of the test suite in order to verify different behaviours without having to go and build an additional mock.

Perhaps this time next year I'll have found yet another way to write mocks, but for the moment it's really nice to be doing so using what, for this project at least, is effectively a base feature of the language I'm writing in.

UNIX is my IDE. Perl5 is my VM. CPAN is my language.

-- mst, out.