How to test Perl roles without creating test classes

Recently I’ve been working on a game engine which uses a composition pattern for its actors. I’m using Role::Tiny to create the roles. Role::Tiny is really convenient as it lets you use roles with native OO Perl, without committing to a whole object system like Moose. A typical role looks like this:

package March::Attribute::Id;
use 5.020;
use Role::Tiny;
use feature 'signatures';
no warnings 'experimental';

sub id ($self)
{
    $self->{id};
}

1;

All this role does is return the id attribute of the consuming class (yes I’m using signatures throughout). I wanted to write unit tests for this role, but I didn’t want to a create test class to test the role. So how do you construct an object from a package that has no constructor? The answer is by using bless in your test file:

use strict;
use warnings;
use Test::More;

my $self = bless { id => 5 }, 'March::Attribute::Id';

BEGIN { use_ok 'March::Attribute::Id' }

is $self->id, 5, 'id()';

done_testing();

This code creates an object called $self by blessing a hashref with the package name of the role that I want to test. It adds a key value pair for the id attribute, and then tests that the role’s id method returns the correct id value. I can execute the tests using prove:

$ prove -vl t/Attribute/Id.t 
t/Attribute/Id.t .. 
ok 1 - use March::Attribute::Id;
ok 2 - id()
1..2
ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.02 cusr  0.00 csys =  0.04 CPU)
Result: PASS

This is another role I want to test:

package March::Attribute::Direction;
use 5.020;
use Role::Tiny;
use feature 'signatures';
no warnings 'experimental';
use March::Game;
use March::Msg;

requires 'id';

sub direction ($self, $new_direction = 0)
{
    if ($new_direction && $new_direction->isa('Math::Shape::Vector'))
    {
        $self->{direction} = $new_direction;

        # publish direction to game queue
        March::Game->publish(
            March::Msg->new(__PACKAGE__, $self->id, $new_direction)
        );
    }
    $self->{direction};
}

1;

This role gets and sets the direction vector for the consuming class. The challenge with testing this role is that it requires the consuming class to implement an id method. Role::Tiny’s requires function is a great way to ensure that the consuming class meets the requirements of the role. But how do we test it without creating a real class with an id sub? What I do is declare the required sub in the test file:

use strict;
use warnings;
use Test::More;
use Math::Shape::Vector;

# create an object
my $self = bless { direction => Math::Shape::Vector->new(1, 2) 
                  }, 'March::Attribute::Direction';

# add required sub
sub March::Attribute::Direction::id { 107 };

BEGIN { use_ok 'March::Attribute::Direction' }

is $self->direction->{x}, 1, 'Check direction x is 1';
is $self->direction->{y}, 2, 'Check direction y is 2';
ok $self->direction( Math::Shape::Vector->new(1, 0) ),
    'Update direction to new vector';
is $self->direction->{x}, 1, 'Check direction x is still 1';
is $self->direction->{y}, 0, 'Check direction y is now 0';

done_testing();

The magic line is sub March::Attribute::Direction::id { 107 }; which adds the sub to the role I’m testing (it just returns the value 107). Now I can test the direction method, again using prove:

$ prove -lv t/Attribute/Direction.t 
t/Attribute/Direction.t .. 
ok 1 - use March::Attribute::Direction;
ok 2 - Check direction
ok 3 - Check direction
ok 4 - Update direction to new vector
ok 5 - Check direction
ok 6 - Check direction
1..6
ok
All tests successful.
Files=1, Tests=6,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.08 cusr  0.00 csys =  0.10 CPU)
Result: PASS

It’s not all gravy

One drawback I’ve encountered with this approach can be seen with the following role and test file:

package Data::Inspector;
use Role::Tiny;

sub inspect_data
{
    my ($self, $data);
    Data::Dumper->Dump(['Inspecting:', $data]);
}

1;

This role has a method called inspect_data which simply returns a dump of any data reference pass to it. This is the test file:

use Test::More;
use Data::Dumper;

my $self = bless {}, 'Data::Inspector';

BEGIN { use_ok 'Data::Inspector' } 

ok $self->inspect_data({ test => 'data' });

done_testing();

As before I bless the role in the test file and then proceed to test the inspect_data method. This test file runs and all the tests pass. Can you spot this issue here? Notice that the Data::Inspector role uses Data::Dumper’s Dump method, but it doesn’t load the Data::Dumper module, the test file does! This is a problem as when the Data::Inspector role is used elsewhere in real code, it will crash and burn when it doesn’t find Data::Dumper loaded in memory.

Conclusion

With this project I intend to create a lot of simple roles, so this approach provides a lightweight way for me to test roles within the test file without creating test classes for every role.

I really like Role::Tiny. It’s flexible: you can create minimalist trait-like behavior or go further and create mixins (roles which modify state). It has nice features like auto-enabling strict and warnings, method modifiers and good documentation. Role::Basic is another lightweight roles module that supports traits only (by design). I wonder if I’ll come to regret using a mixin approach as I get further into development of the game engine.

Tags

David Farrell

David is the founder and editor of PerlTricks.com. An organizer of the New York Perl Meetup, he works for ZipRecruiter as a software developer.

Browse their articles