Overriding Built-ins

     

No matter how nice it might be to believe otherwise , not all of the world is under your control. This is particularly true when dealing with Perl's built-in operators and functions, which can wreak havoc on your psyche when you're trying to test your code fully. Your program may need to run a system( ) call and deal with failure gracefully, but how do you test that?

Start by redefining the problem.

How do I do that?

Suppose you have written a module to play songs on your computer. It consists of a class, SongPlayer , that holds a song and the application to use to play that song. It also has a method, play( ) , that launches the application to play the song. Save the following code as lib/SongPlayer.pm :

 package SongPlayer;     use strict;     use warnings;     use Carp;     sub new     {         my ($class, %args) = @_;         bless \%args, $class;     }     sub song     {         my $self      = shift;         $self->{song} = shift if @_;         $self->{song};     }     sub player     {         my $self        = shift;         $self->{player} = shift if @_;         $self->{player};     }     sub play     {         my $self   = shift;         my $player = $self->player(  );         my $song   = $self->song(  );         system( $player, $song ) =  = 0 or              croak( "Couldn't launch $player for $song: $!\n" );     }     1; 

Testing the constructor ( new( ) ) and the two accessors ( song( ) and player( ) ) is easy. Testing play( ) is more difficult for two reasons. First, it calls system( ) , which relies on behavior outside of the testing environment. How can you know which songs and media players people will have on their systems? You could bundle samples with the tests, but trying to support a full-blown media player on all of your target systems and architectures could be painful. Second, system( ) has side effects. If it launches a graphical program, there's no easy way to control it from Perl. To continue the tests, the user will have to exit it manually ”so much for automation.

How can you write this test portably?

When you don't have the world you want, change it. Save this test file as songplayer.t :

 #!perl     use strict;     use warnings;     use lib 'lib';     use Test::More tests => 11;     use Test::Exception;     my $module = 'SongPlayer';     use_ok( $module ) or exit;     can_ok( $module, 'new' );     my $song = $module->new( song => 'RomanceMe.mp3', player => 'xmms' );     isa_ok( $song, $module );     can_ok( $song, 'song' );     is( $song->song(  ), 'RomanceMe.mp3',         'song(  ) should return song set in constructor' );     can_ok( $song, 'player' );     is( $song->player(  ), 'xmms',         'player(  ) should return player set in constructor' );     can_ok( $song, 'play' );     {         package SongPlayer;         use subs 'system';         package main;         my $fail = 0;         my @args;         *SongPlayer::system = sub         {             @args = @_;             return $fail;         };         lives_ok { $song->play(  ) } 'play(  ) should live if launching succeeds';         is_deeply( \@args, [qw( xmms RomanceMe.mp3 )],             'play(  ) should launch player for song' );         $fail = 1;         throws_ok { $song->play(  ) } qr/Couldn't launch xmms for RomanceMe.mp3/,             '... throwing exception if launching fails';     } 

Run it with prove :

 $  prove songplayer.t  songplayer....ok     All tests successful.     Files=1, Tests=11,  0 wallclock secs ( 0.10 cusr +  0.01 csys =  0.11 CPU) 

What just happened ?

Instead of launching xmms to play the song, the test overrode the system( ) operator with a normal Perl subroutine. How did that happen?


Note: The forward declaration could take place at the top of the test file; it's in the play( ) test for clarity .

The subs pragma allows you to make forward declarations of subroutines. It tells Perl to expect user-defined subroutines of the given names. This changes how Perl reacts when it encounters those names . In effect, this snippet:

 use subs 'system'; 

hides the built-in system( ) in favor of a user-defined system( ) , even though the definition happens much later as the test runs!

The test file performs one trick in using the subs pragma. It changes to the SongPlayer package to execute the pragma there, and then changes back to the main package. The other interesting part of the code is the definition of the new system( ) function:

 my $fail = 0;     my @args;     *SongPlayer::system = sub     {         @args = @_;         return $fail;     }; 

It's a closure, closing over the $fail and @args variables. Both the enclosing block and the function can access the same lexical variables . Setting $fail in the block changes what the function will return. The mocked system( ) function sets @args based on the arguments it receives. Together, they allow the test to check what play( ) passes to system( ) and to verify that play( ) does the right thing based on the dummied-up return value of the mocked function.

Mocking system( ) allows the test to force a failure without the tester having to figure out a failure condition that will always run on every supported platform.

What about...

Q:

This seems invasive. Is there another way to do it without overriding system( ) ?

A:

You can't easily undo overriding. If you cannot isolate the scope of the overriding well ”whether in a block or a separate test file, this can be troublesome .

There's an alternative, in this case. Save the following test file as really_play.t :

 #!perl     use strict;     use warnings;     use lib 'lib';     use Test::More tests => 5;     use Test::Exception;     my $module = 'SongPlayer';     use_ok( $module ) or exit;     my $song = $module->new( song => '77s_OneMoreTime.ogg',         player => 'mpg321' );     $song->song( 'pass.pl' );     is( $song->song(  ), 'pass.pl',         'song(  ) should update song member, if set' );     $song->player( $^X );     is( $song->player(  ), $^X,         'player(  ) should update player member, if set' );     lives_ok { $song->play(  ) } 'play(  ) should launch program and live';     $song->song( 'fail.pl' );     dies_ok { $song->play(  ) }         'play(  ) should croak if program launch fails'; 


Note: The special variable $^X holds the path to the currently executing Perl binary. See perldoc perlvar .

Instead of setting the song and player to an actual song and player, this code uses the currently executing Perl binary and sets the song to either pass.pl or fail.pl . Save this code to pass.pl :

 exit 0; 

and this code as fail.pl :

 exit 1; 

Now when play( ) calls system( ) , it runs the equivalent of the command perl pass.pl or perl fail.pl , checking the command's exit code.

This kind of testing is more implicit; if something goes wrong, it can be difficult to isolate the invalid assumption. Was the name of the file wrong? Was its exit value wrong? However, redefining part of Perl can be treacherous, even if you put the overriding code in its own test file to minimize the damage of violating encapsulation. Using fake programs is gentler and may have fewer unexpected side effects.

Both approaches are appropriate at different times. When you have precise control of how your code communicates with the outside world, it's often simpler to run fake programs through the system( ) command, for example. When it's tedious to exercise all of the necessary behavior of the external program or resource, mocking is easier.



Perl Testing. A Developer's Notebook
Perl Testing: A Developers Notebook
ISBN: 0596100922
EAN: 2147483647
Year: 2003
Pages: 107

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net