Porting Test::Class to the p5-mop

Stevan Little has been asking people to port things to the p5-mop-redux or to create new modules. I decided to reimplement Test::Class::Moose as Test::Class::MOP. The following works:

use mop;

class TestsFor::DateTime extends Test::Class::MOP {
   use DateTime;
   use Test::Most;

   # methods that begin with test_ are test methods.
   method test_constructor($report) {
       $report->plan(3);    # strictly optional

       can_ok 'DateTime', 'new';
       my %args = (
           year  => 1967,
           month => 6,
           day   => 20,
       );
       isa_ok my $date = DateTime->new(%args), 'DateTime';
       is $date->year, $args{year}, '... and the year should be correct';
   }
}
Test::Class::MOP->new->runtests;

While a number of tests fail, you can check it out here on github. The failing tests seem to largely be due to a few places in my tests where I relied on the old Class::MOP. The actual test code appears to work fine.

What follows are my notes (from memory) on the experience.

First, let me just say that p5-mop-redux is wonderful. I've long felt that for Perl to really regain its feet it needs a proper object model and method signatures. This is a great step forward. Here, for example, is my old report class for test classes:

package Test::Class::Moose::Report::Class;

use Moose;
use namespace::autoclean;

with qw(
  Test::Class::Moose::Role::Reporting
);

has test_methods => (
    is      => 'rw',
    traits  => ['Array'],
    isa     => 'ArrayRef[Test::Class::Moose::Report::Method]',
    default => sub { [] },
    handles => {
        all_test_methods => 'elements',
        add_test_method  => 'push',
        num_test_methods => 'count',
    },
);

has 'error' => (
    is        => 'rw',
    isa       => 'Str',
    predicate => 'has_error',
);

__PACKAGE__->meta->make_immutable;

1;

And here's the new one:

use mop;
use strict;
use warnings;

class Test::Class::MOP::Report::Class
 with Test::Class::MOP::Role::Reporting {
    has $!error is rw;
    method has_error { defined $!error }

    has $!test_methods is ro = [];

    method all_test_methods { @{ $!test_methods } }

    method add_test_method($test_method) {
        push $!test_methods => $test_method;
    }
    method num_test_methods {
        return scalar @{ $!test_methods };
    }
}

You'll notice that we don't have type constraints or coercions by default, though it's documented how I can create them.

strict and warnings don't appear to be on by default, and the num_test_methods exposes an interesting parsing issue. I can't use @$!test_methods to dereference the $!test_methods variable, but that's a minor issue that hopefully will be fixed later.

Predicates are no longer needed because we no longer need to worry about the internals. Instead, we can check for definedness directly.

And that role? Here's how it's defined:

use mop;
use strict;
use warnings;

role Test::Class::MOP::Role::Reporting with Test::Class::MOP::Role::Timing {
    has $!name    is ro = die "name is required";
    has $!notes   is ro = {};
    has $!skipped is rw; 

    method is_skipped { defined $!skipped }
}

This is how I previously had defined it:

package Test::Class::Moose::Role::Reporting;

use Moose::Role;
with 'Test::Class::Moose::Role::Timing';

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

has 'notes' => (
    is      => 'rw',
    isa     => 'HashRef',
    default => sub { {} },
);

has skipped => (
    is        => 'rw',
    isa       => 'Str',
    predicate => 'is_skipped',
);

1;

Notice that we simply have a default "die" assignment to get required attributes. Also, roles are properly consuming other roles seamlessly.

I decided to be less magical in Test::Class::MOP, so it doesn't load Test::Most for you. Also, Sub::Attribute didn't play well with p5-mod-redux, so attributes are not supported. That being said, I'm sure there are ways I can eventually work around that.

One thing I really appreciated is that BUILDARGS is no longer required! I previously had this in my Test::Class::Moose:

around 'BUILDARGS' => sub {
    my $orig  = shift;
    my $class = shift;
    return $class->$orig(
        { test_configuration => Test::Class::Moose::Config->new(@_) } );
};

That's because I wanted to minimize namespace pollution (so test classes wouldn't accidentally override important methods) and I pushed the constructor arguments into a configuration class. With the new mop, I did this:

method new ($class: @args) {
    $class->next::method(
        test_configuration => Test::Class::MOP::Config->new(@args) 
    );
}

My method to fetch the test classes is a bit clumsy:

method test_classes {
    # some extra code here ... 
    state $classes;
    unless ($classes) {
        $classes = [
            sort
            grep { $_ ne 'Test::Class::MOP' && $_->isa('Test::Class::MOP') }
            map { s!/!::!g; s/\.pm$//; $_ } keys %INC
        ];
    }

    # eventually we'll want to control the test class order
    return @$classes;
}

I had to hard-code the class name because __PACKAGE__ returns the package name, not the class name (yay! Classes aren't packages any more) :) Stevan suggested I use __CLASS__ instead, but it was giving me a bareword compile-time error. This might be a parsing bug, but more likely, I suspect I missed something.

I also suspect there's a better way of figuring out which classes are test classes rather than searching through the entire %INC hash, but while I used the mop::meta code a couple of times, figuring this one out eluded me.

All things considered, my various classes and roles wound up being smaller and cleaner, aside from the lack of type checking. I also find the code easier to read. Porting Test::Class::Moose only took a couple of hours and was very easy.

I tried 5.19.5, but twigils wouldn't compile, so this code was run on 5.18.1.

If you want to learn more, Damien Krotkine has a nice, gentle introduction to the mop.

2 Comments

I tried to port some simple module to mop. it is relative easy and a little "boring". I end up write a small scripts to do some work for me. However, I wish there is a better script to transform Moose to mop. I am not a very good perl programmer. do not know how to hack to moose and parse the code and then do the transform. I just use regex match and produce mop code from attributes in moose.

Leave a comment

About Ovid

user-pic Freelance Perl/Testing/Agile consultant and trainer. See http://www.allaroundtheworld.fr/ for our services. If you have a problem with Perl, we will solve it for you.