No more copy and paste: How to refactor tests with roles

Raise your hand if you've ever cut and paste a huge chunk of code — or even a whole file — for testing. I have. And I feel guilty, because I know the DRY mantra: "Don't repeat yourself!" But somehow, rules we follow for our application code, we forget for our test code.

Here's a recent example from my own Dancer2::Plugin::Queue. It provides a role, Dancer2::Plugin::Queue::Role::Queue, that any particular backend must implement. I wrote a trivial implementation and some tests for it.

Then it was time to write a backend: Dancer2::Plugin::Queue::MongoDB. And write tests for it. (Can you see where this is going?)

The easiest thing would be to copy the .t files from Dancer2::Plugin::Queue to Dancer2::Plugin::Queue::MongoDB. And that's exactly what I didn't want to do.

What did I do instead? I wrote my tests as a role.

What are roles?

If you've used Moose or Moo, then you should already know. If you haven't, then in a nutshell, a Role is a composable unit of behavior. It is not a class, but you can combine roles as part of a class. If you think of inheritance defining an "is-a" relationship, instead a role defines a "does-a" relationship. A class "does" a role, or many roles.

Moose and Moo roles can have attributes and methods just like classes. They can also require methods to be provided by the class that consumes them. This allows for easy, powerful, reusable behavior encapsulation.

Why are roles helpful in testing? When should you use them?

If you think about it, a test describes behavior. Given X, does it do Y? So roles are a very natural way to model and encapsulate tests. A test role can define attributes necessary for conducting the tests (aka "fixtures"), any necessary state initialization, and the tests themselves.

Not every test needs to be written as a role. Even repetitive tests can be written procedurally. Lots of my test files look more or less like this:

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

my @cases = (
    {
        label => "first case",
        inputs => { ... },
        expected => { ... },
    },
    ...
);

for my $c ( @cases ) {
   # do lots of tests given inputs and expectations
}

Roles are really powerful when you want to test similar behaviors in different *.t files or even different distributions. If the guts of that for loop above would need to be repeated, then that's a good sign that roles might be a good abstraction.

Even then, you might not need roles. There's nothing wrong with a test library containing subroutines. For me, roles are the right choice when I need to start passing a lot of state to my tests, or when I have to do a lot of setup or tear down around my tests.

How can I use roles in testing?

Instead of writing a test library, you could write a test class (or more than one) and use Moose or Moo roles to compose your behaviors. But once you've started to consider that, you should instead jump straight to one of these modules, which make it really easy:

Which one? Either one. I'm slightly biased because I wrote Test::Roo, but I did it because I wanted something like Test::Routine for non-Moose projects where I didn't want Moose as a test dependency. If you get familiar with Test::Roo, then you can use it for either Moose or Moo based projects, which I think is convenient.

Ovid has recently released Test::Class::Moose. I know nothing about it, so can't recommend it, but Ovid is a smart guy who knows a lot about large-scale testing, so it might be worth investigating as well.

When shouldn't I use these modules?

While they offer some nice syntactic sugar even in simple cases, they do add some complexity that might not be worth it for simple tests. While both Moose and Moo are popular, powerful OO frameworks, you might not want to depend on them just for your tests. (Obviously, if your code already uses Moose or Moo, then using Test::Routine or Test::Roo is a no-brainer.)

Examples

I'll give some example from the Test::Roo::Cookbook. These are also in the 'examples' directory of the repository and CPAN distribution.

Intro to Test::Roo

This example introduces you to Test::Roo. It doesn't actually use any roles, but you need to grok this one to follow the later ones. I'll annotate it with comments more heavily than in the original.

# loading Test::Roo loads Moo, strictures and Test::More, and makes the current
# package (main) a subclass of Test::Roo::Class
use Test::Roo;

use MooX::Types::MooseLike::Base qw/ArrayRef/;
use Path::Tiny;

# here is a fixture -- a Moo attribute for a text file to use for testing; note that
# it is 'required', so it must be provided in the constructor arguments 
has corpus => (
    is       => 'ro',
    isa      => sub { -f shift },
    required => 1,
);

# another fixture; this one is lazy and caches lines from the corpus on demand
has lines => (
    is  => 'lazy',
    isa => ArrayRef,
);

# here is the builder that actually loads the lines
sub _build_lines {
    my ($self) = @_;
    return [ map { lc } path( $self->corpus )->lines ];
}

# this is the first test -- whether the lines of the corpus are sorted;  the
# first call to 'lines' caches the lines of the file so it is only read
# once for the life of the test object
test 'sorted' => sub {
    my $self = shift;
    is_deeply( $self->lines, [ sort @{$self->lines} ], "alphabetized");
};

# this is the second test -- whether all letters are accounted for in the corpus;
# it uses the cached lines from the first test
test 'a to z' => sub {
    my $self = shift;
    my %letters = map { substr($_,0,1) => 1 } @{ $self->lines };
    is_deeply( [sort keys %letters], ["a" .. "z"], "all letters found" );
};
 
# this instantiates the current package into an object (with the required argument)
# and runs all the tests
run_me( { corpus => "/usr/share/dict/words" } );

# tell Test::More that there are no more tests
done_testing;

Creating a test role with Test::Roo

Now that you're a little familiar with Test::Roo syntax, I'll give a real example with a test role, based on how I might test a file-finder like Path::Iterator::Rule.

I didn't actually write Path::Iterator::Rule tests like this, because I hadn't written Test::Roo yet. In hindsight, I wish I had. Just writing the example for the cookbook revealed a bug affecting subclasses.

Testing a file finder requires setting up a temporary directory, creating a bunch of files within it, and testing what files or directories are found for a given set of rules. Path::Iterator::Rule was designed to be subclassed, so that I could turn Path::Class::Rule into a subclass that worked with Path::Class objects.

Right away, I can tell that this is good for role-based testing. Lots of state (list of files to create) needed for the test? Check! Lots of setup (creating the files)? Check! Needs to be reusable by subclasses? Check!

I'll discuss the test role file in sections.

First, we set up the test role as a package, load in Test::Roo::Role for a standalone role, and bring in the dependencies:

package IteratorTest;
use Test::Roo::Role;
 
use MooX::Types::MooseLike::Base qw/:all/;
use Class::Load qw/load_class/;
use Path::Tiny;

Then, we define two required attributes -- the iterator class to test and the "result type" that indicates whether files found are objects and if so, of what type.

has [qw/iterator_class result_type/] => (
    is       => 'ro',
    isa      => Str,
    required => 1,
);

Next, we define a list of test files to create and a temporary directory to hold them. While the list has a default value, note that it could be changed during object construction for more specialized testing.

has test_files => (
    is      => 'ro',
    isa     => ArrayRef,
    default => sub {
        return [
            qw(
            aaaa
            bbbb
            cccc/dddd
            eeee/ffff/gggg
            )
        ];
    },
);
 
has tempdir => (
    is  => 'lazy',
    isa => InstanceOf ['Path::Tiny']
);

We also need a object for the iterator we are testing. This one is lazy and will just be an instance of the iterator_class.

 
has rule_object => (
    is      => 'lazy',
    isa     => Object,
    clearer => 1,
);

For all our lazy attributes, we now need some builders. I'll add some extra comments to annotate them.

# gives our Test::More subtest a dynamically determined label
sub _build_description { return shift->iterator_class }

# creates the temporary directory *and* creates all the files in it
sub _build_tempdir {
    my ($self) = @_;
    my $dir = Path::Tiny->tempdir;
    $dir->child($_)->touchpath for @{ $self->test_files };
    return $dir;
}
 
# creates the empty rule object
sub _build_rule_object {
    my ($self) = @_;
    load_class( $self->iterator_class );
    return $self->iterator_class->new;
}

Up to this point, the role has just been defining requirements and fixtures. Next, it's time to define test behaviors.

First, there is a method to test result types, since that needs some different logic depending on whether objects or strings are returned. This isn't a 'test' declaration, but it will be called from them later.

sub test_result_type {
    my ( $self, $file ) = @_;
    if ( my $type = $self->result_type ) {
        isa_ok( $file, $type, $file );
    }
    else {
        is( ref($file), '', "$file is string" );
    }
}

Finally, we get a simple test definition that checks if finding all files gets the same list of files used to create the testing directory.

test 'find files' => sub {
    my $self = shift;
    $self->clear_rule_object; # make sure have a new one each time
 
    $self->tempdir;
    my $rule = $self->rule_object;
    my @files = $rule->file->all( $self->tempdir, { relative => 1 } );
 
    is_deeply( \@files, $self->test_files, "correct list of files" )
    or diag explain \@files;
 
    $self->test_result_type($_) for @files;
};

# ... more tests ...

1;

If we had more tests, we'd add them after that initial test.

If I were doing this "for real", I would refactor the temp directory construction into a separate role, so that I could have different roles for different behaviors without having to repeat my fixture code, either. Roles composing roles FTW! This is a great use for roles even without even considering the benefits for subclass testing.

Given that test role, testing it for a given implementation in a *.t file just looks like this:

use Test::Roo;
use lib 'lib';
 
with 'IteratorTest';
 
run_me(
    {
        iterator_class => 'Path::Iterator::Rule',
        result_type    => '',
    }
);
 
done_testing;

And then the subclass *.t file is the same thing, just with a different set of constructor arguments.

use Test::Roo;
use lib 'lib';
 
with 'IteratorTest';
 
run_me(
    {
        iterator_class => 'Path::Class::Rule',
        result_type    => 'Path::Class::Entity',
    },
);
 
done_testing;

Test role for Dancer2::Plugin::Queue

Now that you've seen the basics of Test::Roo, I'll show you how I wrote my Dancer2::Plugin::Queue test role. For the blog, I'll strip out some the bits less relevant to using test roles (including the actual server setup), but you can see all of it in Dancer2::Plugin::Queue::Role::Test.

package Dancer2::Plugin::Queue::Role::Test;

use Test::Roo::Role;
use MooX::Types::MooseLike::Base qw/Str HashRef CodeRef/;

use Dancer2 ':syntax';
use Dancer2::Plugin::Queue;
use HTTP::Tiny;
use Test::TCP;

has backend => (
    is       => 'ro',
    isa      => Str,
    required => 1,
);

has options => (
    is      => 'lazy',
    isa     => HashRef,
);

sub _build_options { }

has _server => (
    is  => 'lazy',
    isa => CodeRef,
);

sub _build__server {
    my ($self) = @_;
    return sub { ... }; # details elided for the blog post
}

test 'queue and dequeue' => sub {
    my $self = shift;
    test_tcp(
        client => sub {
            my $port = shift;
            my $url  = "http://localhost:$port/";
            my $ua = HTTP::Tiny->new;
            my $res = $ua->get( $url . "add?msg=Hello%20World" );
            like $res->{content}, qr/Hello World/i, "sent and receieved message";
        },
        server => $self->_server,
    );
};

1;

The *.t file just overrides the options builder method, composes the test role, and invokes it for the trivial "Array" backend. It looks like this:

use Test::Roo;
 
sub _build_options { return { name => 'foo' } }
 
with 'Dancer2::Plugin::Queue::Role::Test';
 
run_me( { backend => 'Array' } );
 
done_testing;

Summary

If you don't like copy-and-paste coding, roles are a great way to compose behaviors for testing. If you're already used to Moose or Moo — or using them in your code — now it's easy to use roles for testing, too.

Oh, yeah, remember that queue plugin, Dancer2::Plugin::Queue::MongoDB, the one where I was thinking to copy and paste the test? I've already shown you the test role. Now I'll show you how I used it.

Most of the code below is just fixture setup — making sure there is a MongoDB and clearing a test database for the test run. Again, I'll annotate it a bit more than the original.

use Test::Roo;
use MooX::Types::MooseLike::Base qw/:all/;

use MongoDB 0.45;
use MongoDBx::Queue;

# We use this client for checking for a DB and dropping tables before a run;
# it's lazy so we can test creating it in an eval and skip tests otherwise
has client => (
    is => 'lazy',
    isa => InstanceOf['MongoDB::MongoClient'],
);

sub _build_client {
    MongoDB::MongoClient->new;
}

has db_name => (
    is => 'ro',
    isa => Str,
    default => sub { 'test_dancer_plugin_queue_mongodb' },
);

# this overrides the test role's builder to set the right options in the server
# for this backend implementation
sub _build_options {
    my ($self) = @_;
    return { db_name => $self->db_name };
}

# Here's where we actually check that the database exists.  We try to
# build the lazy client attribute, and skip tests if it fails.
sub BUILD {
    my ($self) = @_;
    eval { $self->client }
        or plan skip_all => "No MongoDB on localhost";
}

# I didn't show this part of Test::Roo, but this runs before any tests
# to ensure the test table is dropped
before setup => sub {
    my $self = shift;
    my $db   = $self->client->get_database($self->db_name);
    my $coll = $db->get_collection('queue');
    $coll->drop;
};

# This runs after all tests to clean up
after teardown => sub {
    my $self = shift;
    my $db   = $self->client->get_database($self->db_name);
    $db->drop;
};

# This composes the test role for us to run
with 'Dancer2::Plugin::Queue::Role::Test';

run_me({ backend => 'MongoDB' });
done_testing;

See? The .t files is all fixture management and the test behaviors stay in the role from the superclass. As the test role gets better or bugs are fixed, the tests for the implementations get better, too.

I don't have to copy and paste a thing.

My advice to you

If you made it to the end of this long article, I'll guess that you're intrigued or maybe even excited. Here's what you need to do next:

  • Install Test::Roo. Go on. Do it. Right now.
  • Copy the first example from the cookbook into a file. (Install Path::Tiny while you're at it.)
  • Run it! Then tweak the code and run it again.

That's all you need to get started. Try splitting the test from that file out into a role in another .pm file and compose it back to the .t file using with. Run it again!

Now that you see how it works, the next time you are about to cut and paste some test code, try roles instead.

Good luck, and stay DRY!

Additional Reading

This entry was posted in perl programming and tagged , , , . Bookmark the permalink. Both comments and trackbacks are currently closed.

10 Comments

  1. dly
    Posted March 18, 2013 at 9:50 pm | Permalink

    My favorite part was :)

    is_deeply( \@files, $self->test_files, "correct list of files" )
    or diag explain \@files;

    It never occured to me to use the "or die" idiom with tests to display debugging print or diag or note statements when a test fails like this.

    It's a good idea, but then I'd have to copy and paste "or diag(...);" everywhere!

    I also hate copy and paste testing. I have done nothing but (for testing). A friend of mine from college coined the term CPC - Copy Paste Customize. Where he would copy and paste VBA examples for MS Access applications, and then customize them to work for our project. This is obviously a huge no-no, but everyone does it for test suites, but somehow knows better for their actual application.

    I'm considering some sort of macro approach, but using Test::Roo or Test::Routine seems even better except for the Moo or Moose tax. Perhaps a Test::R or something with a better name for Role::Tiny?

    • Posted March 19, 2013 at 7:46 am | Permalink

      I'm willing to pay at least the Moo tax for any reasonably complex piece of code because it's already pretty minimal and I want the rest of the benefits it gives me like lazy attributes.

      If I didn't want that, I'd just use a test library and do it procedurally or whip up cheap objects by hand.

    • Abigail
      Posted March 19, 2013 at 9:58 am | Permalink

      A friend of mine from college coined the term CPC - Copy Paste Customize. Where he would copy and paste VBA examples for MS Access applications, and then customize them to work for our project. This is obviously a huge no-no

      Why is that "obviously" a huge no-no?

      I can certainly understand that it isn't always a good idea, but "obviously a huge no-no" seems way to dogmatic for me. And accepting dogmas is a sign for refusing to think for yourself.

      I use 'CPC' a lot. Probably far often than most programmers would do. (Do note that while I say I use it a lot, doesn't mean I always do it, in many cases I will not). There are some advantages. For instance, it saves time *now*. Sure, I know it may mean I pay with more time later, but 1) that's not certain, and 2) costs now are more expensive that costs in the future (the time I save now I can invest in something that makes money now, which is much better than making money later). Second, by using CPC instead of wrapping the code in a subroutine that can handle both situations (the orginal, and the "new" case) I end up with code that's less complex (although longer), and, since I leave the existing code as it is, there's far less chance that I break the existing functionality. Third, the argument is often make that if you wrap duplicated (or duplicated and slightly modified) code in a subroutine, it makes it easier to add/change functionality and/or add bug fixes. But this is only partially true. It's certainly true if the new or changed functionality is required for all the cases the subroutine is called. But it also happens that functionality needs to be changed or added for some of the cases in which the subroutine is called, and not in others. Then it becomes harder to make changes, and you run the risk of breaking the cases that don't need a chance.

      IMO, adhering to dogmas like "DRY" is *not* a sign of a good programmer. A good programmer will make a judgement call each and every time.

      • Posted March 19, 2013 at 10:31 am | Permalink

        Adhering to any dogma is a sign that someone isn't thinking and that's a bad sign in a programmer.

        That doesn't mean, however, that DRY isn't a good guidelines. Repetitive code, or copy-paste code should be a warning to start thinking to make those tradeoffs you describe.

        In the article, I explain when I think roles are appropriate and when they aren't appropriate because I don't want to encourage a new "all tests must be roles" dogma.

  2. Phill
    Posted March 19, 2013 at 7:34 am | Permalink

    Another article that reinforces how Moose::Roles ridiculous "flexibility" can be used to reduce copy and paste and help generally save key strokes.

    "Moose and Moo roles can have attributes and methods just like classes. They
    can also require methods to be provided by the class that consumes them.
    This allows for easy, powerful, reusable behavior encapsulation."

    ...actually you are clearly *reducing* encapsulation because you're merging two different, "supposed to be isolated" objects (or pieces of behaviour) into one fat, sloppy piece of shit. It is NOT behaviour encapsulation it is the OPPOSITE of behaviour encapsulation. You should be using composition. You know this and you even consider it, I believe, before coming up with this weak justification:

    "For me, *roles are the right choice when I
    need to start passing a lot of state to my tests, or when I have to do a
    lot of setup or tear down around my tests*."

    This isn't a legitimate justification. The nature of OO programming allows you to put lots of state into one object you can pass around. Your use of "For me" does seem like you knew my comment was coming. It's laziness pure and simple right?

    Phill

    • Posted March 19, 2013 at 7:58 am | Permalink

      tl;dr: You don't like roles. So don't use them.

      The reason that roles don't wind up a "fat, sloppy piece of shit" the way most mixin-style OO approaches do is because the composition resolves conflicts and requirements — or at least forces the programmer to resolve them. You can be sure that roles have composed cleanly.

      You suggest "composition" -- meaning objects holding objects. That, too, easily winds up a sloppy nested, pile of shit, with your top level object having to collect arguments and cascade them down, or cascading method calls or delegation patterns. It can be useful, but it's not universally useful any more than roles are.

      I said "for me" because I know that roles have a higher cognitive cost — they require more application design than just banging out Test::More procedures like usual. I didn't want anything thinking I was saying this is the only way to do it or should be a default way to code tests. I was explaining why and when *I* find it useful. That's all.

      Others can make that judgment for themselves. If you don't care for roles, then clearly this isn't for you.

      • Phill
        Posted March 19, 2013 at 9:54 am | Permalink

        I'm totally for people solving their own problems but unfortuntely I work in a large Perl development team and although I'd enjoy dictating everything to everyone it's sadly not a possibility. Instead, I find myself, having to discuss the style and convince other people it isn't the way.

        Part of my frustration is that, whilst maybe, it's okay for you and your test suite, we overuse roles in our company. Tired of saying get_db_connection? We've added a WithSchema role. Now it's totally acceptable from an OO perspective to do $printer->database. It's an advertised service. We've got WithLogger, WithMessageFactory. Just to save keystrokes and copy and paste, like your original intentions.

        With this excessive overuse, the purpose of a role is lost and the original intention of being a way of defining contracts between objects becomes subverted to the point where you can't reuse the roles or trust them because their behaviour is locked in by the classes that consume them. Especially after requirements change. Understanding interactions becomes difficult. Understanding where to put new responcibility becomes hard. You are making one object from two (or more), therefore you are directly implying those two source files are for all intents and purposes part of the same thing. That they together form one unit and feed off the same state.

        And all to save some typing. It's Perl Golf, enterprise edition.

        Whilst it might genuinely seem like a good idea in this case, and I totally see why you think that, and even more so for the test suite, my personal opinion, and warning to you, is that I that I think they lead to a slippery slope where everyone, by using roles, thinks they suddenly 'get object oriented programming' without actually asking themselves if they are building objects with coherent interfaces or controllers with sensible APIs.

        It's a nice article and a good trick to know.. but it is a trick IMHO and shouldn't be people's default solution to increasing code reuse over composition. My concern is that this article may reinforce my collegues' ideas that you can horizontally shard an object into smaller roles just so the files "look prettier".

        You can do so much with Moose::Roles, inherit from them, apply them dynamically, aggregate them together, define functions and variables in them, that they actually allow you to escape an object orientated discipline and enter some crazy world of scope-free programming where you can pull variables out of the air. As I said before, this can be a slippy slope.

        • Posted March 19, 2013 at 10:39 am | Permalink

          I see where you're coming from. The "WithX" pattern would concern me, too, because it implies "has-a" not "does-a" composition.

          I don't think the "lock-in" you describe is any worse than other sorts of OO decompositions, though. If your object subclasses a parent or contains other objects, you're just as dependent on them to provide a consistent API as you are if you consume a role.

  3. Posted March 19, 2013 at 5:54 pm | Permalink

    "rules we follow for our application code, we forget for our test code"

    Yes, that's been bugging me lately too, that's why I decided to start applying "test first" methodology in writing my tests. I've been having a bit of trouble getting the project done, though.

    • Posted March 19, 2013 at 7:41 pm | Permalink

      I like TDD to a point. When I don't know what I want the API to be like, writing tests is a good way to prototype it. But I try to write tests in small chunks so I'm writing "real" code at the same time.

© 2009-2014 David Golden All Rights Reserved