in reply to Re: Re: Re: Documenting Methods/Subs
in thread Documenting Methods/Subs

If you're enforcing the object type you have to draft in Test::MockObject, which is great, but is also substantially more complex.

You can still use the self-shunt unit testing pattern with enforced object types and Test::Class if you take advantage of perl's run-time method dispatch.

For example, consider a class that takes an optional logging object to allow custom logging on an object by object basis.

package Object; use Params::Validate qw(:all); sub new { my $class = shift; my %self = validate(@_, {logger => { isa => 'Logger' } } ); return( bless \%self, $class ); }; # ... more code ... sub log { my ($self, @messages) = @_; $self->{logger}->log($self, @messages); };

One way to test Object's log method would be to create a mock Logger object with Test::MockObject and pass this to Object->new. Instead, we will make the Test::Class pretend to be a Logger object.

package Object::Test; use base qw(Test::Class); use Test::More; # run $code while pretending that __PACKAGE__ ISA $class sub _do_as(&$) { my ($code, $class) = @_; { # we have to make sure the package hash exists # for the method dispatch to work no strict 'refs'; *{"$class\::"}{HASH} }; local @Object::Test::ISA = ($class, @Object::Test::ISA); $code->(); }; sub setup : Test(setup) { my $self = shift; _do_as { $self->{object} = Object->new(logger => $self) } 'Logger' +; }; sub test_show_answer : Test(3) { my $self = shift; _do_as { $self->{object}->log("hello", "there") } 'Logger'; }; sub log { my $self = shift; is($_[0], $self->{object}, 'passed object to logger'); is($_[1], "hello", 'passed arg1'); is($_[2], "there", 'passed arg2'); };

Sneaky - eh?

Replies are listed 'Best First'.
Re: Test::Class & self-shunt pattern
by pdcawley (Hermit) on Jan 13, 2003 at 07:14 UTC
    Cute trick. It's actually something I'd considered but rejected (but I would say that wouldn't I?), but your wrapper is very neat.

    The problem with it, as I see it is that, with optimisitic typing (ie, just passing in $self), you get an immediate failure if the object under test calls an unexpected method simply because you haven't implemented it. With a localized @ISA, Logger's methods becomes callable and an unexpected method call could get dispatched to real code (and you can't be sure that that would throw an exception.

    If Params::Validate does the right thing ($obj->isa('foo'), not UNIVERSAL::isa($obj, 'foo')) then you could always do

    sub _do_as (&$) { my($block, $fake_class) = @_; local *Object::Test::isa = sub { my($self, $target_class) = @_; return $target_class eq $fake_class || $self->SUPER::isa($target_class); }; $block->(); }
    If the validater calls UNIVERSAL::isa, you could override that instead, but it would be rather more awkward. And it's all a good deal more awkward than just chucking the test object in without adornment.

    This really comes down to how much value you see in the strict typing of variables. Personally I am unconvinced by it. I'd prefer to see a good test suite (and Smalltalk style method selectors, but they're rather harder to come by in Perl. Smalltalk method selectors are great though. They give you lots more naming opportunities, your 'new' method would become

    newWithLogger: aLogStream ^ self new setLogger: aLogStream
    which isn't necessarily the most convincing of arguments, but believe me it's great with more complex method signatures...)

      The problem with it, as I see it is that, with optimisitic typing (ie, just passing in $self), you get an immediate failure if the object under test calls an unexpected method simply because you haven't implemented it.

      Fair point. Although in some circumstances that could be what you want :-)

      Instead of replacing stuff I sometimes just add instrumentation with something like Hook::LexWrap - handy when retrofitting tests onto tightly coupled code.

      This really comes down to how much value you see in the strict typing of variables.

      Well, all the ways of doing it in perl suck in one way or another so I don't often use it, but it's nice if done right (Eiffel springs to mind).

      Method selecters are cool, and interfaces would be yet another solution.

      Maybe in perl6 :-)

        Hook::LexWrap is very cool certainly. I'd not thought of using it for hammering tests into tightly coupled code, but now you mention it, it's a bloody good idea.
        Method selecters are cool, and interfaces would be yet another solution.
        I happen to know there's someone working on method selectors in Perl 5. And, despite having written the Perl 6 RFC calling for Interface support, I'm becoming less and less convinced by them. Again, in a self shunt context, if you declare that a test case implements the 'foo' interface then you're obliged to implement all the methods in the 'foo' signature if you want the thing to compile, which can be somewhat painful, especially when you're adding the nth interface to your test class.