Porting Test::Builder to Perl 6
by chromatic
|
What Went Wrong
Writing the base for all (or at least many) possible test modules is
tricky. In this case, it was trebly so. Not only was this the first bit of
practical OO Perl 6 code I'd written, but I had no way to test it, either
by hand (how I tested the Perl 5 version, before Schwern and I worked out a
way to write automated tests for it), or with automated tests. Pugs didn't
even have object support when I wrote this, though checking in this code
pushed OO support higher on the schedule.
Infinite Loops in Construction
Originally, I thought all test classes would inherit from
Test::Builder::Test. As Damian Conway pointed out, my technique created an
infinite loop. (He suggested that "Don't make a façade factory class an
ancestor of the instantiable classes" is a design mistake akin to "Don't get
involved in a land war in Asia" and mumbled something else about battles of
wits and Sicilians.) The code looked something like:
class Test::Builder::Test
{
my Test::Builder::Test $:singleton is rw;
has Bool $.passed;
has Int $.number;
has Str $.diagnostic;
has Str $.description;
method new (Test::Builder::Test $class, *@args)
{
return $:singleton if $:singleton;
$:singleton = $class.create( @args );
return $:singleton;
}
method create(
$number,
$passed = 1,
?$skip = 0,
?$todo = 0,
?$reason = '',
?$description = '',
)
{
return Test::Builder::Test::TODO.new(
description => $description, reason => $reason, passed => $passed,
) if $todo;
return Test::Builder::Test::Skip.new(
description => $description, reason => $reason, passed => 1,
) if $skip;
return Test::Builder::Test::Pass.new(
description => $description, passed => 1,
) if $passed;
return Test::Builder::Test::TODO.new(
description => $description, passed => 0,
) if $todo;
}
}
class Test::Builder::Test::Pass is Test::Builder::Test {}
class Test::Builder::Test::Fail is Test::Builder::Test {}
class Test::Builder::Test::Skip is Test::Builder::Test { ... }
class Test::Builder::Test::TODO is Test::Builder::Test { ... }
# ...
Why is this a singleton? I have no idea; I typed that code into the
wrong module and continued writing code a few minutes later, thinking
that I knew what I was doing. The infinite loop stands out in my mind very
clearly now. Because all of the concrete test classes inherit from
Test::Builder::Test, they inherit its new() method; none of them
override it. Thus, they'll all call create() again (and none of
them override that either).
Confusing Initialization
I also struggled with the various bits and pieces of creating and
building objects in Perl 6. There are a lot of hooks and overrides
available, making the object system very flexible. However, without any
experience or examples or guidance, choosing between new(),
BUILD(), and BUILDALL() is difficult.
I realized I had no idea how to handle the singleton in Test::Builder.
At least, when I realized that (for now) Test::Builder could remain a
singleton, I didn't know how or where to create it.
I finally settled on putting it in new(), with code much
like that in the broken version of Test::Builder::Test previously.
new() eventually allocates space for, creates, and returns an
opaque object. BUILD() initializes it. This led me to write
code something like:
class Test::Builder;
# ...
has Test::Builder::Output $.output;
has Test::Builder::TestPlan $.plan;
has @:results;
submethod BUILD ( Test::Builder::Output ?$output, ?$TestPlan )
{
$.plan = $TestPlan if $TestPlan;
$.output = $output ?? $output :: Test::Builder::Output.new();
}
There's a difference here because most uses of Test::Builder set the
test plan explicitly later, after receiving the Test::Builder object. I
added a plan() method, too:
method plan ( $self:, Str ?$explanation, Int ?$num )
{
die "Plan already set!" if $self.plan;
if ($num)
{
$self.plan = Test::Builder::TestPlan.new( expect => $num );
}
elsif $explanation ~~ 'no_plan'
{
$self.plan = Test::Builder::NullPlan.new();
}
else
{
die "Unknown plan";
}
$self.output.write( $self.plan.header() );
}
There are some stylistic errors in the previous code. First, when
declaring an invocant, there's a colon but no comma. Second,
fail is much better than die (an assertion Damian
made that I take on faith, having researched more serious issues instead).
Third, the parenthesization of the cases in the if statement
is inconsistent.
Prev [1] [2] [3] Next