I've been trying out Moose on a hobby project, and I'd like to get
some feedback on the way I'm unit testing my roles.  I've ended up
writing a couple of role/mock test support modules, and I suspect I've
reinvented some wheels in the process.

How do you unit test your roles ?

In my app, roles define interfaces and classes that do the roles
implement the interfaces.  I use method modifiers in the roles, for
example:

# In ZF::Role::Foo
before 'foo' => sub {
    my $self = shift;

    $self->has_foo_prerequisite or $self->setup_foo_prerequisite;
};

For each such role I have a mock implementation.  The mock
implementations are useful to test code that needs an object that does
the role, and they also contain unit tests to ensure that the role
code itself is doing the right thing:

# In ZF::Mock::Foo
sub mock_testcount_foo { 1 }
sub foo {
    my $self = shift;

    if (my $t = $self->mock_test) {
        ok $self->has_foo_prerequisite, 'role fixed prereq'.$t->tns;
    }

    ...
}

The mock_test() method is inherited from ZF::Mock, and it returns
false if this mock object is not in run-internal-unit-tests mode,
otherwise it returns an object.  $t->tns returns a Test Name Suffix.

The ZF::Mock baseclass adds a mock_method_plan attribute, which is
used to declare how many times each method of the mock class is
expected to be called.  In combination with mock_testcount_foo() type
per-method test count declarations, this makes it possible to
calculate the number of tests that will be run.

The test scripts are supported by a somewhat Test::Class-like module,
which handles setting up mock objects with their method plans:

use Test::More;
use ZF::Test::TestSet;

use ZF::Mock::Foo;

testset 2, 'basic', ['ZF::Mock::Foo', {foo => 2}], sub {
    my $foo = mockobj(bar => 'yes');
    is $foo->foo, 'foo', 'foo is foo';
    is $foo->foo, 'foo', 'foo is still foo';
};

... and that's a complete test script.  The support modules calculate
the test plan and run the tests in an END block.

The mockobj() call consumes the class name and method plan from the
arrayref, and returns the mock object.  Those have to be declared
outside the testset sub so that they are available for calculating the
test plan before the sub is executed.

I find that this approach eliminates the pain of debugging a bad test
count, since the support modules add tests to check that each testset
runs the right number of tests, that each mock method is called the
right number of times and that the method runs the right number of
tests on each call.

Sometimes the method call profile varies with the test input, and
calculating the method plan is a bit of a pain.  I'm half convinced
that it's worth it, since tests that make strong assertions about the
set of method calls that reach the implementing class are a good thing
when testing roles that use "around", right ?

Since testset is a runtime thing, I can call it in a loop if the
method plan varies with test input:

foreach my $input (keys %input_data) {
    testset, 1, "foo $input", ['ZF::Mock::Foo',
compute_method_plan($input)], sub {
        my $tns = shift;

        my $foo = mockobj(stuff => $input_data{$input});
        1 while $foo->munge;
        is $foo->foo, 'foo', 'foo is foo'.$tns;
    };
}

--ZF

Reply via email to