Porting Test::Builder to Perl 6
by chromaticJuly 28, 2005
Perl 6 development now proceeds in two directions. The first is from the bottom up, with the creation and evolution of Parrot and underlying code, including the Parrot Grammar Engine. The goal there is to build the structure Perl 6 will need. The second direction is from the top down, with the Pugs project implementing Perl 6 initially separate from Parrot, though recent additions allow an embedded Parrot to run the parsed code and to emit valid Parrot PIR code.
Both projects are important and both help the design of Perl 6 and its implementation. Parrot is valuable in that it demonstrates a solid foundation for Perl 6 (and other similar languages); a far better foundation than the internals of Perl 5 have become. Pugs is important because it allows people to use Perl 6 productively now, with more features every day.
|
Related Reading
Perl Testing: A Developer's Notebook |
Motivation and Design
Perl culture values testing very highly. Several years ago, at the suggestion of Michael Schwern, I extracted the code that would become Test::Builder from Test::More and unified Test::Simple and Test::More to share that back end. Now dozens of other testing modules, built upon Test::Builder, work together seamlessly.
Pugs culture also values testing. However, there was no corresponding Test::Builder for Perl 6 yet--there was only a single Test.pm module that did most of what the early version of Test::More did in Perl 5.
Schwern and I have discussed updates and refactorings of Test::Builder for the past couple of years. We made some mistakes in the initial design. As Perl 6 offers the chance to clean up Perl 5, so does a port of Test::Builder to Perl 6 offer the chance to clean up some of the design decisions we would make differently now.
Internally, Test::Builder provides a few testing and reporting functions and keeps track of some test information. Most importantly, it contains a plan consisting of the number of tests expected to run. It also holds a list of details of every test it has seen. The testing and reporting functions add information to this list of test details. Finally, the module contains functions to report the test details in the standard TAP format, so that tools such as Test::Harness can interpret the results correctly.
Test::Builder needs to do all of these things, but there are several ways to design the module's internals. Some ways are better than others.
The original Perl 5 version mashed all of this behavior together into
one object-oriented module. To allow the use of multiple testing modules
without confusing the count or the test details,
Test::Builder::new() always returns a singleton. All test
modules call the constructor to receive the singleton object and call the
test reporting methods to add details of the tests they handle.
This works, but it's a little inelegant. In particular, modules that test test modules have to go to a lot of trouble to work around the design. A more flexible design would make things like Test::Builder::Tester much easier to write.
The biggest change that Schwern and I have discussed is to separate the varying responsibilities into separate modules. The new Test::Builder object in Perl 6 itself contains a Test::Builder::TestPlan object that represents the plan (the number of tests to run), a Test::Builder::Output object that contains the filehandles to which to write TAP and diagnostic output, and an array of tests' results (all Test::Builder::Test instances).
The default constructor, new(), still returns a singleton
by default. However, modules that use Test::Builder can create their own
objects, which perform the Test::Builder::TestPlan or Test::Builder::Output
roles and pass them to the constructor to override the default objects
created internally for the singleton. If a test module really needs a
separate Test::Builder object, the alternate create() method
creates a new object that no other module will share.
This strategy allows the Perl 6 version of Test::Builder::Tester
to create its own Test::Builder object that reports tests as normal and then
creates the shared singleton with output going to filehandles it can read
instead of STDOUT and STDERR. The design appears to be sound; it took less
than two hours to go from the idea of T::B::T to a fully working
implementation--counting a break to eat ice cream.
First Attempts
Translating Perl 5 OO code into Perl 6 OO code was mostly straightforward, despite my never having written any runnable Perl 6 OO code. (Also, Pugs was not far enough along that objects worked.)
What Went Right
One nice revelation is that opaque objects are actually easier to work with than blessed references. Even better, Perl 6's improved function signatures reduce the necessity to write lots of boring boilerplate code.
Breaking Test::Builder into separate pieces gave the opportunity for
several other refactorings. One of my favorite is "Replace Condititional with
Polymorphism". There are four different types of tests that have different
reporting styles: pass, fail, SKIP, and TODO. It made sense to create separate
classes for each of those, giving each the responsibility and knowledge to
produce the correct TAP output. Thus I wrote Test::Builder::Test, a
façade factory class with a very smart constructor that creates and
returns the correct test object based on the given arguments. When
Test::Builder receives one of these test objects, it asks it to return the TAP
string, passes that message to its contained Test::Builder::TestOutput object,
and stores the test object in the list of run tests.


