Inside-Out Failure Injection

| 1 Comment

While it's still true that lexical scope is the fundamental unit of encapsulation in Perl 5, dynamic scope is a powerful tool.

Consider this snippet from a Catalyst application controller:

sub activate :Chained('superget') :PathPart('activate') :Args(0)
{
    my ($self, $c) = @_;
    my $proj       = $c->stash->{project};

    try
    {
        $proj->activate_project( $c->stash->{user} );
        $c->add_status( 'Activated project ' . $proj->project_name );
    }
    catch { $c->add_error( $_ ) };

    $self->redirect_to_action( $c, 'view', '', [ $proj->id ] );
}

Ignore almost everything but the Try::Tiny code (the try/catch blocks). The dynamic scope of this exception handling code means any exception thrown from either method called from the try block or any other code they call, will result in the activation of the catch block. Outside of those two blocks, any exceptions are the responsibility of something else.

That's easy to understand, but take it a step further and think of this dynamic scope as some sort of multiverse behavior. (If this metaphor doesn't work, that's fine. It's how I think of it.) Within a delimited scope representing program flow, not source code, the universe has changed.

Exceptions aren't the only use for dynamic scope, and even dynamic scopes offer the possibility for encapsulation.I wrote some interesting code the other day while refactoring out accumulated duplication and improving the test coverage of the controller. My goal was to test the error handling if the activation failed. This presented a design opportunity: how could I force a failure of the activation? As you can see from the controller, the project object is already in the stash. I could override Catalyst's dispatch mechanism or internals to create a dummy project object which dies on activation. I could extract these controller tests into tests which don't go through the web interface. I could even use an environment variable to change the behavior of $project->activate to throw an error.

Instead, I used an injected monkeypatch. The resulting test looks like:

    test_result_override
    {
        $ua->get_ok( 'http://localhost/projects/2/activate',
            'failed activation' );
        $ua->content_contains( 'Activation failed',
            '... should contain error message' );
    } Project => activate_project => sub { die 'Activation failed' };

$ua is an instance of Test::WWW::Mechanize::Catalyst.

You can probably see where this is going. Within the block passed to test_result_override, the Project object's activate_project method is the function passed as the final argument—a function which throws an exception.

The monkeypatch injector was likewise relatively easy to write:

sub test_result_override(&@)
{
    my ($test, $class, $subname, $sub) = @_;

    no strict 'refs';

    my $ref = "Appname::Schema::Result::${class}::${subname}";
    local *{ $ref };
    *{ $ref } = $sub;

    $test->();
}

The function prototype of &@ tells the Perl 5 parser to treat the block as a function reference. The rest of the code merely performs the monkeypatching with local (so that the monkeypatch will go away when this function returns, however it returns).

Once I knew what this code should do (I wrote the code which used it first), it took two minutes to write, and maybe five minutes to use in the other places in this controller I needed it. This isn't always the best approach, but this sufficiently encapsulated monkeypatch injection is sufficiently powerful and useful for helping ensure that my code behaves as I intended.

1 Comment

This idiom is fairly common in Emacs lisp.

It's usually a macro that temporarily changes _something_ during the execution of a piece of code you pass in. The naming convention for this is with-*, e.g.

* with-temp-buffer
* with-output-to-string
* with-demoted-errors

I quite like it, and have used it in Perl to e.g. encapsulate the ugly setting of global %ENV variables that change behaviour elsewhere.


/J

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

affiliated with ModernPerl.net

Categories

Pages

About this Entry

This page contains a single entry by chromatic published on July 15, 2011 11:58 AM.

Free Tools for Free Books was the previous entry in this blog.

M0 Test Tasks: Stealing from Perl 6 is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Sponsored by Blender Recipe Reviews and the Trendshare how to invest guide

Powered by the Perl programming language

what is programming?