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.
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.
Hao, I think the only way you could really transform Moose to mop programatically is to write code to inspect Class::MOP and generate code from that. However, mop deliberately doesn't support some of the things that Moose does, so it wouldn't entirely work. Getting it "close enough" and fine-tuning by hand is the way to go, so you're doing it right.