Stupid Testing Trick: Inconstant Constants

I have a piece of Perl containing manifest constants that have the same value, but which signify different things. In my testing, I wanted to make sure I was getting the right one out of my code. That seems to mean changing code to test it, which is anathema to me.

It occurred to me today that if I held my tongue right I could change the values of the constants without touching the code they were defined in or exported from. Holy aspect-oriented programming, Batman!

The trick is based on the fact that what use constant really does is to define a constant subroutine with an empty prototype. Now, Perl will allow me to redefine a subroutine -- grumpily if use warnings qw{redefine} is in effect, and happily if not. So I thought all I had to do was load the module that exported the constant, hammer its symbol table, and voila!

As an example, assume module Foo exports manifest constants PLATFORM and MAGIC, both having value 42, but intended for different purposes. To make this trick work, my test needs to:

use Foo ();    # NO IMPORTS!

{
no warnings qw{ redefine };
sub Foo::PLATFORM () { 9.75 };
sub Foo::MAGIC () { 8 };
}

use Foo qw{ PLATFORM MAGIC };

$\ = "\n";
print PLATFORM; # Platform 9-3/4
print MAGIC; # The number between 7 and 9

This works with Perl 5.27.1, and as far back as 5.6.2, though under some earlier versions you may need to localize $^W and set it false. Under all Perls I have tried, running the script under -MO=Deparse shows that the revised value really is inlined.

I am not sure I am brave enough (or foolish enough) to put something like this in production testing, but for author testing it seems pretty handy.

6 Comments

If you don't want to deal with the ugly redefine warnings yourself, you can use Class::Method::Modifiers to wrap an existing function to return something different:

use Foo ();
use Class::Method::Modifiers;
around 'Foo::PLATFORM' => sub () { 9.75 };
around 'Foo::MAGIC' => sub () { 8 };

If you are doing this often for some reason, you could even define a Role::Tiny to apply these modifiers to the package (this would create a new package though, and not affect the original Foo). But definitely still something to do only in tests, and not in production code unless absolutely necessary :)

You'd need to do the 'around's in a BEGIN{} block for them to take effect before the constants are important from Foo in the 'use' line.

imported*. And you don't need to call them as methods, just accessing Foo::MAGIC should work regardless of importing.

Though on second thought Foo::MAGIC may get compiled out due to being a constant, before the 'around' takes effect if it wasn't done in BEGIN{}.

Leave a comment

About Tom Wyant

user-pic I blog about Perl.