I have no idea if this is silly, or if everyone-and-their-dog already does this, or if this is novel. Basically, I wanted a way to have a "controlled scope" ââ¬âàI wanted my mock to live only as long as I wanted it to, but I didn't want to have that be within a lexical constraint. (Naturally, if all I cared about were a lexical scope, I would have used local
)
So what I did was create a "scoping token". It's just an empty scalar that has a destructor "attached" to it to handles the cleanup. I suppose this could have been done using overloading (a la Scalar::Defer) but this solution seemed like it would be faster and cleaner.
Here's my method mock_class_constructor()
. It takes as its arguments the name of the constructor symbol you are mocking (that is, a fully qualified method name) and the object (assumably a Test::MockObject, but could be anything) that you want it to return.
sub mock_class_constructor { my $proto = shift; my($constructor_symbol, $return_object) = @_; no strict 'refs'; no warnings 'redefine'; my $orig_constructor = \&{ "$constructor_symbol" }; *{ "$constructor_symbol" } = sub { $return_object }; # orderly cleanup my $package = int(rand(time)) . int(rand($$)); no strict 'refs'; *{ $package . '::DESTROY' } = sub { # Actual cleanup no warnings 'redefine'; *{ "$constructor_symbol" } = $orig_constructor; # Cleanup of this closure undef *{ $package . '::DESTROY' }; }; return bless \do { my $anon_scalar }, $package; }
So now in my test code, when I want to unit test with the boundary class mocked, I call this first. If I want to switch to a longer integration test, I'll release the token and run this again on a different class (further down the stack.) (Naturally, I don't need this at all for a full-stack integration test.)
Yes, I know I could have had the method accept a closure instead of just an object to return. At that point though I'd just be re-implementing Test::MockModule.
My original design was to have the method return a callback closure that does the cleanup. I dumped this for two reasons:
With this design, I could do any number of more "natural" scope controls:
local()
)weaken()
the $return_object
reference otherwise the closure will have a circular reference.So I feel like I've got something that might have some useful application, but I also imagine that it's a dead end someone else has already found. I'm interested in any feedback.
Scoping token in action
dpisoni on 2006-10-27T04:50:09
I can give a little, though I think in most cases I didn't use it in as novel ways as I conceived that I would. (I'll have to redact a bit, but that's okay.) But I have a mean over-engineering streak...
The method in the journal entry exists in my project Testing superclass (which isa Test::Class.) In addition, there is:
sub mock_network {
my $proto = shift;
$proto->mock_class_constructor( NETWORK_PKG() . '::new', @_ );
}
sub mock_implementation {
my $proto = shift;
$proto->mock_class_constructor( IMPLEMENTATION_PKG() . '::new', @_ );
}These are for mocking the layers of the library I'm testing (it's a specialized web service API.) In the class that tests the public API layer, I have:
sub startup : Test(startup => 1) {
my $self = shift;
$self->SUPER::startup();
my $test_class = $self->{test_class} = $TEST_CLASS;
require_ok( $test_class) or $self->BAILOUT("Can't test class we can't require");
my $impl_mocker = $self->{impl_mocker} = Test::MockObject->new();
$impl_mocker->mock(... );
# etc etc
}And later...
sub test_03a_list_things : Tests {
my $self = shift;
my $cleanup_token = $self->mock_implementation( $self->{impl_mocker} );
$self->_list_things(@_);
}
sub test_03b_list_things_integration : Tests {
my $self = shift;
my $net_mocker = $self->gen_net_mocker();
my $cleanup_token = $self->mock_network( $net_mocker );
$self->_list_things( $net_mocker, @_ );
}The actual test code is in the _list_things() method. I'm just using lexical scoping here, but I get to move all the code that does the mocking to one place, and yet still have a lexical scope. (If I had used local, I would have had to repeat the actual mocking code every time I want to do this. Bad DRY!)
Most of what I've done looks like this, it appears. But hey! Think of what I could do! I think I'm remembering now my initial motivation: it was DRY, per the example above.