On Tuesday, November 12, 2002, at 01:40  am, Michael G Schwern wrote:

On Tue, Nov 12, 2002 at 01:31:43AM +0000, Mark Fowler wrote:
Test::Builder->new would remain as a singleton. We'd just provide an
alternate constructor to provide a second object if someone really wants it.
You know, that would at the very least tidy up Test::Builder::Tester
somewhat.  It's currently just a bit of a hack...(sssh, I didn't really
say that)

That's two votes.
[snip]

Make that three votes :-) I've been meaning to write a little missive
on why Test::Builder shouldn't be a singleton for some time.

Three reasons why I would like Test::Builder not to be a singleton:


1) Makes testing tests much easier.

If you can have the code you're testing use one object while your
tests use another then testing tests becomes considerably less
baroque.

Some of the tests in Test::Class are quite evil - even with the help
of Test::Builder::Tester (thanks Mark :-). Things like capturing test
headers would be easy if I had access to a completely separate builder
object.


2) Makes it easy to collect a set of test results and use them as a
meta-test.

For example, for one project I did a few months back I needed to write
some tests in a declarative style (done a bit before Test::ManyParams
was around unfortunately). I didn't want to reimplement the normal
test subroutines, so I went about it by collecting test results.

E.g.:

sub pass_all (&@) {
my ($sub, $name) = @_;

Test::Builder->no_ending(1); # since we mess with current_test too much
my $builder = Test::Builder->new;

my $old_test = $builder->current_test;
my $io = IO::File->new_tmpfile or die "bad tmp file ($!)";
my ($output, $failure_out) = ($builder->output, $builder->failure_output);

eval {
$builder->output($io);
$builder->failure_output($io);
$sub->();
};

my $exception = $@;
my $failed = grep {$_ == 0} ($builder->summary)[$old_test .. $builder->current_test-1];
$builder->output($output);
$builder->failure_output($failure_out);
$builder->current_test($old_test);
die $exception if $exception;

$builder->ok(!$failed, $name);
if ($failed) {
seek $io, SEEK_SET, 0;
while (<$io>) {
next if m/^#\s+Failed test (.*?at line \d+)/;
chomp;
s/^((not )?ok)\s+\d+/$1/;
s/^# //;
$builder->diag(" $_")
};
};
};

allows me to do things like:

pass_all {
ok(1==2, "1==2");
ok(2==2, "2==2");
} 'maths is hard';

producing

not ok 2 - maths is hard
# Failed test (declarative.pl at line 31)
# not ok - 1==2
# ok - 2==2

While this works, it's not nice (and some related subs were worse).
Having to mess with no_ending because we're rewinding the current_test
number and "running" more tests than we end up reporting I find
especially nasty.

It would all be simpler if I could have had another Test::Builder object
for the tests that I was examining.


3) Subclassing Test::Builder would be useful.

For example, I would like to make the default name for tests run under
Test::Class to be the name of the method enclosing the test.

With Test::Builder as a singleton my only option is something evil
like using Hook::LexWrap to wrap the Test::Builder ok() method. If I
can make and use a Test::Builder subclass all I need to do is override
ok() in the subclass.


How should it work? Straw man:

my $builder = Test::Builder->default
Return the default test builder object, creating a new one if
necessary.

Test::Builder->default($new)
Set a new default builder object

$Test::Builder::Default
Tied scalar that returns/sets the default test builder object

Test::Builder->new
Returns the tied scalar $Test::Builder::Default. That way we can
change the default of existing Test::Builder based modules that
initialise a single builder object at compile time.

Test::Builder->create
Really create a new Test::Builder object.

This would allow me to rewrite pass_all() something like this:

sub pass_all (&@) {
my ($sub, $name) = @_;

my $io = IO::File->new_tmpfile or die "bad tmp file ($!)";

my $failed;
{
local $Test::Builder::Default = Test::Builder->create;
$Test::Builder::Default->output($io);
$Test::Builder::Default->failure_output($io);
$sub->();
$failed = grep {$_ == 0} $Test::Builder::Default->summary;
};

my $builder = Test::Builder->default;
$builder->ok(!$failed, $name);

if ($failed) {
seek $io, SEEK_SET, 0;
while (<$io>) {
next if m/^#\s+Failed test (.*?at line \d+)/;
chomp;
s/^((not )?ok)\s+\d+/$1/;
s/^# //;
$builder->diag(" $_")
};
};
};

Which I think is much nicer! Hopefully this make sense and sleep
depravation isn't making me talk nonsense again ;-)

Cheers,

Adrian

Reply via email to