Chapter 5. Testing Untestable Code

One of the precepts of good unit testing is to test individual pieces of code in isolation. Besides helping to ensure that your code works, this testing improves your design by decoupling unrelated modules and enforcing communication among well-defined and, hopefully, well-tested interfaces. It also makes debugging failed tests easier by reducing the number of failure points.

Testing in isolation is difficult, though. Most applications have some degree of interdependence between components, being the sum of individual pieces that don’t always make sense when isolated from the whole. An important pattern of behavior in testing is mocking : replacing untestable or hard-to-test code with code that looks like the real thing but makes it easier to test. Perl’s easygoing nature allows you to poke around in other people’s code in the middle of a program without too much trouble.

This chapter’s labs demonstrate how to change code—even if it doesn’t belong to you or if it merely touches what you really want to test—in the middle of your tests. Though fiddling with symbol tables and replacing variables and subroutines is very powerful, it is also dangerous. It’s too useful a tool not to consider, though. Here’s when, why, and how to do it safely.

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: $!
" );
    }

    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.

Mocking Modules

Sometimes two or more pieces of code play very nicely together. This is great—until you want to test them in isolation. While it’s good to write testable code, you shouldn’t have to go through contortions to make it possible to write tests. Sometimes it’s okay for your tests to poke through the abstractions, just a little bit, to make sure that your code works the way you think it ought to work.

Being a little bit tricky in your test code—in the proper places and with the proper precautions—can make both your code and your tests much simpler and easier to test.

How do I do that?

Suppose that you want to search for types of links in HTML documents. You’ve defined a class, LinkFinder, whose objects contain the HTML to search as well as an internal parser object that does the actual HTML parsing. For convenience, the class uses the LWP::Simple library to fetch HTML from a web server when provided a bare URI.

Save the following code as lib/LinkFinder.pm:

    package LinkFinder;

    use URI;
    use LWP::Simple ();
    use HTML::TokeParser::Simple;

    sub new
    {
        my ($class, $html) = @_;
        my $uri            = URI->new( $html );

        if ($uri->scheme())
        {
            $html = LWP::Simple::get( $uri->as_string() );
        }

        my $self = bless { html => $html }, $class;
        $self->reset();
    }

    sub parser
    {
        my $self = shift;
        return $self->{parser};
    }

    sub html
    {
        my $self = shift;
        return $self->{html};
    }

    sub find_links
    {
        my ($self, $uri) = @_;
        my $parser       = $self->parser();

        my @links;

        while (my $token = $parser->get_token() )
        {
            next unless $token->is_start_tag( 'a' );
            next unless $token->get_attr( 'href' ) =~ /Q$uriE/;
            push @links, $self->find_text();
        }

        return @links;
    }

    sub find_text
    {
        my $self   = shift;
        my $parser = $self->parser();

        while (my $token = $parser->get_token())
        {
            next unless $token->is_text();
            return $token->as_is();
        }

        return;
    }

    sub reset
    {
        my $self        = shift;
        my $html        = $self->html();
        $self->{parser} = HTML::TokeParser::Simple->new( string => $html );

        return $self;
    }

    1;

Save the following test file as findlinks.t:

    #!perl

    use strict;
    use warnings;

    use lib 'lib';

    use Test::More tests => 11;
    use Test::MockModule;

    my $module = 'LinkFinder';
    use_ok( $module ) or exit;
    my $html   = do { local $/; <DATA> };

    my $vanity = $module->new( $html );
    isa_ok( $vanity, $module );
    is( $vanity->html(), $html, 'new() should allow HTML passed in from string' );

    {
        my $uri;
        my $lwp = Test::MockModule->new( 'LWP::Simple' );
        $lwp->mock( get => sub ($) { $uri = shift; $html } );

        $vanity = $module->new( 'http://www.example.com/somepage.html' );
        is( $vanity->html(), $html, '... or from URI if passed' );
        is( $uri, 'http://www.example.com/somepage.html',
            '... URI passed into constructor' );
    }

    my @results = $vanity->find_links( 'http' );
    is( @results, 3, 'find_links() should find all matching links' );
    is( $results[0], 'one author',     '... in order'              );
    is( $results[1], 'another author', '... of appearance'         );
    is( $results[2], 'a project',      '... in document'           );

    $vanity->reset();
    @results    = $vanity->find_links( 'perl' );
    is( @results, 1,              'reset() should reset parser'    );
    is( $results[0], 'a project', '... allowing more link finding' );

    _ _DATA_ _
    <html>
    <head><title>some page</title>
    <body>
    <p><a href="http://wgz.org/chromatic/">one author</a></p>
    <p><a href="http://langworth.com/">another author</a></p>
    <p><a href="http://qa.perl.org/">a project</a></p>
    </body>

Note

The test declares $uri outside of the mocked subroutine to make the variable visible outside of the subroutine.

Note

See Special Literals in perldoc perldata to learn about __DATA__.

Run it with prove:

    $ prove findlinks.t
    findlinks....ok
    All tests successful.
    Files=1, Tests=11,  0 wallclock secs ( 0.21 cusr +  0.02 csys =  0.23 CPU)

What just happened?

When LinkFinder creates a new object, it creates a new URI object from the $html parameter. If $html contains actual HTML, the URI object won’t have a scheme. If, however, $html contains a URL to an HTTP or FTP site containing HTML, it will have a scheme. In that case, it uses LWP::Simple to fetch the HTML.

Note

The anonymous subroutine has a prototype to match that of LWP::Simple::get(). Perl will warn about a prototype mismatch without it. You only need a prototype if the subroutine being mocked has one.

You can’t rely on having a reliable network connection every time you want to run the tests, nor should you worry that the remote site will be down or that someone has changed the HTML and your tests will fail. You could run a small web server to test against, but there’s an easier solution.

The Test::MockModule module takes most of the tedium out of overriding subroutines in other packages (see "Overriding Live Code,” later in this chapter). Because LinkFinder uses LWP::Simple::get() directly, without importing it, the easiest option is to mock get() in the LWP::Simple package.

The test creates a new Test::MockModule object representing LWP::Simple. That doesn’t actually change anything; only the call to mock() does. The two arguments passed to mock() are the name of the subroutine to override—get, in this case—and an anonymous subroutine to use for the overriding.

Within the new scope, all of LinkFinder’s calls to LWP::Simple::get() actually call the anonymous subroutine instead, storing the argument in $uri and returning the example HTML from the end of the test file.

Note

What if you decide to import get() in LinkFinder after all? Pass ‘LinkFinder’ to the Test::MockModule constructor instead.

The rest of the test is straightforward.

What about...

Q: What if you write mostly object-oriented code? How do you mock classes and objects?

A: See "Mocking Objects,” next.

Mocking Objects

Some programs rely heavily on the use of objects, eschewing global variables and functions for loosely-coupled, well-encapsulated, and strongly polymorphic designs. This kind of code can be easier to maintain and understand—and to test. Well-factored code that adheres to intelligent interfaces between objects makes it possible to reuse and substitute equivalent implementations—including testing components.

This lab demonstrates how to create and use mock objects to test the inputs and outputs of code.

How do I do that?

The following code defines an object that sends templated mail to its recipients. Save it as lib/MailTemplate.pm:

    package MailTemplate;

    use strict;
    use Email::Send 'SMTP';

    sub new
    {
        my ($class, %args) = @_;
        bless \%args, $class;
    }

    BEGIN
    {
        no strict 'refs';

        for my $accessor (qw( message recipients sender sender_address server ))
        {
            *{ $accessor } = sub
            {
                my $self   = shift;
                return $self->{$accessor};
            };
        }
    }

    sub add_recipient
    {
        my ($self, $name, $address) = @_;
        my $recipients              = $self->recipients();
        $recipients->{$name}        = $address;
    }

    sub deliver
    {
        my $self       = shift;
        my $recipients = $self->recipients();

        while (my ($name, $address) = each %$recipients)
        {
            my $message = $self->format_message( $name, $address );
            send( 'SMTP', $message, $self->server() );
        }
    }

    sub format_message
    {
        my ($self, $name, $address) = @_;

        my $message    = $self->message();
        my %data       =
        (
            name           => $name,
            address        => $address,
            sender         => $self->sender(),
            sender_address => $self->sender_address(),
        );

        $message =~ s/{(w+)}/$data{$1}/g;
        return $message;
    }

    1;

Note

The BEGIN trick here is like using AUTOLOAD to generate accessors, except that it runs at compile time for only those accessors specified.

Using this module is easy. To send out personalized mail to several recipients, create a new object, passing the name of your SMTP server, your name, your address, a templated message, and a hash of recipient names and addresses.

Testing this module, on the other hand, could be tricky; it uses Email::Send (specifically Email::Send::SMTP) to send messages. You don’t want to rely on having a network connection in place, nor do you want to send mail to some poor soul every time someone runs the tests, especially while you develop them.

What’s the answer?

Save the following test code to mailtemplate.t:

    #!perl

    use strict;
    use warnings;

    use Test::More tests => 23;
    use Test::MockObject;

    use lib 'lib';

    $INC{'Net/SMTP.pm'} = 1;
    my $module          = 'MailTemplate';
    my $message         = do { local $/; <DATA> };

    use_ok( $module ) or exit;

    can_ok( $module, 'new' );
    my $mt = $module->new(
        server         => 'smtp.example.com',
        sender         => 'A. U. Thor',
        message        => $message,
        sender_address => '[email protected]',
        recipients     => { Bob => '[email protected]' },
    );
    isa_ok( $mt, $module );

    can_ok( $mt, 'server' );
    is( $mt->server(), 'smtp.example.com',
        'server() should return server set in constructor' );

    can_ok( $mt, 'add_recipient' );
    $mt->add_recipient( Alice => '[email protected]' );

    can_ok( $mt, 'recipients' );
    is_deeply( $mt->recipients(),
               { Alice => '[email protected]', Bob => '[email protected]' },
               'recipients() should return all recipients' );

    can_ok( $mt, 'deliver' );

    my $smtp = Test::MockObject->new();
    $smtp->fake_module( 'Net::SMTP', new => sub { $smtp } );
    $smtp->set_true( qw( mail to data -quit ) );
    $mt->deliver();

    my %recipients =
    (
        Alice => '[email protected]',
        Bob   => '[email protected]', 
    );

    while (my ($name, $address) = each %recipients)
    {
        my ($method, $args) = $smtp->next_call();
        is( $method,    'mail',              'deliver() should open a mail' );
        is( $args->[1], '[email protected]','... setting the From address' );

        ($method, $args) = $smtp->next_call();
        is( $method,    'to',                    '... then the To address'  );
        is( $args->[1], $address,                '... for the recipient'    );

        ($method, $args) = $smtp->next_call();
        is( $method,      'data',             '... sending the message'     );
        like( $args->[1], qr/Hello, $name/,   '... greeting the recipient'  );
        like( $args->[1], qr/Love,.A. U. Thor/s,
              '... and signing sender name' );
    }

    _ _DATA_ _
    To: {address}
    From: {sender_address}
    Subject: A Test Message

    Hello, {name}!

    You won't actually receive this message!

    Love,
    {sender}

Note

Don’t make assumptions about hash ordering; you’ll have random test failures when you least expect them. Sort all data retrieved from hashes if the order matters to you.

Then run it:

    $ prove mailtemplate.t
    mailtemplate....ok
    All tests successful.
    Files=1, Tests=23,  1 wallclock secs ( 0.16 cusr +  0.02 csys =  0.18 CPU)

What just happened?

The test file starts with a curious line:

    $INC{'Net/SMTP.pm'} = 1;

Note

To prevent MailTemplate from loading Email:: Send, the code to set %INC must occur before the use_ok() call. If you call use_ok() in a BEGIN block, set %INC in a BEGIN block too.

This line prevents the module from (eventually) loading the Net::SMTP module, which Email::Send::SMTP uses internally. %INC is a global variable that contains entries for all loaded modules. When Perl loads a module, such as Test::More, it converts the module name into a Unix file path and adds it to %INC as a new key. The next time Perl tries to load a file with that name, it checks the hash. If there’s an entry, it refuses to load the file again.

Note

%INC has a few other complications. See perldoc perlvar for more details.

If Perl doesn’t actually load Net::SMTP, where does the code for that package come from? Test::MockObject provides it:

    my $smtp = Test::MockObject->new();
    $smtp->fake_module( 'Net::SMTP', new => sub { $smtp } );

The first line creates a new mock object. The second tells Test::MockObject to insert a new function, new(), into the Net::SMTP namespace. Because Email::Send::SMTP uses Net::SMTP::new() to retrieve an object and assumes that it has received a Net::SMTP object, this is the perfect place to substitute a mock object for the real thing.

Of course, when Email::Send::SMTP tries to call methods on the mock object, it won’t do the right thing unless the mock object mocks those methods. Test::MockObject has several helper methods that mock methods on the object. set_true() defines a list of methods with the given names:

Note

To prevent MailTemplate from loading Email:: Send, the code to set %INC must occur before the use_ok () call. If you call use_ok () in a BEGIN block, set %INC in a BEGIN block too.

    $smtp->set_true( qw( mail to data -quit ) );

Each method mocked this way returns a true value. More importantly, they all log their calls by default, unless you prefix their names with the minus character (-). Now Email::Send::SMTP can call mail(), to(), data(), and quit(), and $smtp will log information about the calls for all but the last.

Logging is important if you want to see if the module being tested sends out the data you expect. In this case, it’s important to test that the message goes to the correct recipients from the correct sender, with the template filled out appropriately. Use next_call() to retrieve information about the logged calls:

    my ($method, $args) = $smtp->next_call();
    is( $method,    'mail',               'deliver() should open a mailer' );
    is( $args->[1], '[email protected]', '... setting the From address'   );

In list context, next_call() retrieves the name of the next method called, as well as an array reference containing the arguments to the call. These two tests check that the next method called is the expected one and that the first argument, after the invocant, of course, is the expected From address.

What about...

Q: This test code seems to depend on the order of the calls within Email::Send::SMTP. Isn’t this fragile? What if changes to the module break the tests?

A: That’s one drawback of mock objects; they rely on specific knowledge of the internals of the code being tested. Instead of testing merely that a piece of code does the right thing, sometimes they go further to test how it does what it does.

When possible, designing your code to be more testable will make it more flexible. MailTemplate would be easier to test if its constructor took an object that could send mail. The test could then pass a mock object in through the new() call and perform its checks on that.

However, the real world isn’t always that convenient. Sometimes testing a few parts of a large application with mock objects is the best way to test every part in isolation.

Q: I looked at the Test::MockObject documentation and still don’t understand how to use it. What am I missing?

A: See “A Test::MockObject Illustrated Example” (http://www.perl.com/pub/a/2002/07/10/tmo.html) and “Perl Code Kata: Mocking Objects” (http://www.perl.com/pub/a/2005/04/07/mockobject_kata.html) for more examples.

Q: Do I have to mock all of an object? I only need to change a small part of it.

A: Good thinking. See "Partially Mocking Objects,” next.

Partially Mocking Objects

Mock objects are useful because they give so much control over the testing environment. That great power also makes them potentially dangerous. You may write fantastic tests that appear to cover an entire codebase only to have the code fail in real situations when the unmocked world behaves differently.

Sometimes it’s better to mock only part of an object, using as much real code as possible. When you have well-designed and well-factored classes and methods, use Test::MockObject::Extends to give you control over tiny pieces of code you want to change, leaving the rest of it alone.

How do I do that?

Consider the design of a computer-controlled jukebox for your music collection. Suppose that it holds records, CDs, and MP3 files, with a counter for each item to track popularity. The well-designed jukebox separates storing individual pieces of music from playing them. It has three modules: Jukebox, which provides the interface to select and play music; Library, which stores and retrieves music; and Music, which represents a piece of music.

The Jukebox class is simple:

    package Jukebox;

    use strict;
    use warnings;

    sub new
    {
        my ($class, $library) = @_;
        bless { library => $library }, $class;
    }

    sub library
    {
        my $self = shift;
        return $self->{library};
    }

    sub play_music
    {
        my ($self, $medium, $title) = @_;

        my $class                   = ucfirst( lc( $medium ) );
        my $library                 = $self->library();
        my $music                   = $library->load( $class, $title );
        return unless $music;

        $music->play();
        $music->add_play();

        $library->save( $music, $title, $music );
    }

    1;

Library is a little more complicated:

    package Library;

    use strict;
    use warnings;

    use Carp 'croak';
    use File::Spec::Functions qw( catdir catfile );

    sub new
    {
        my ($class, $path) = @_;
        bless $path, $class;
    }

    sub path
    {
        my $self = shift;
        return $$self;
    }

    sub load
    {
        my ($self, $type, $id) = @_;
        my $directory          = $self->find_dir( $type );
        my $data               = $self->read_file( $directory, $id );
        bless $data, $type;
    }

    sub save
    {
        my ($self, $object, $id) = @_;
        my $directory            = $self->find_dir( $object->type() );
        $self->save_file( $directory, $id, $object->data() );
    }

    sub find_dir
    {
        my ($self, $type) = @_;
        my $path          = $self->path();
        my $directory     = catdir( $path, $type );
        croak( "Unknown directory '$directory'" ) unless -d $directory;
        return $directory;
    }

    sub read_file {  }
    sub save_file {  }

    1;

Finally, the Music class is simple:

    package Music;

    use strict;
    use warnings;

    BEGIN
    {
        @Cd::ISA     = 'Music';
        @Mp3::ISA    = 'Music';
        @Record::ISA = 'Music';
    }

    sub new
    {
        my ($class, $title) = @_;
        bless { title => $title, count => 0 }, $class;
    }

    sub add_play
    {
        my $self = shift;
        $self->{count}++;
    }

    sub data
    {
        my $self = shift;
        return \%$self;
    }

    sub play {  }
    sub type { ref( $_[0] ) }

    1;

Given all of this code, one way to test Jukebox is to mock only a few methods of Library: find_dir(), read_file(), and save_file().

Save the following file as jukebox.t:

    #!perl

    use strict;
    use warnings;

    use Library;
    use Music;

    use Test::More tests => 13;
    use Test::Exception;
    use Test::MockObject::Extends;

    my $lib      = Library->new( 'my_files' );
    my $mock_lib = Test::MockObject::Extends->new( $lib );

    my $module   = 'Jukebox';
    use_ok( $module ) or exit;

    can_ok( $module, 'new' );
    my $jb = $module->new( $mock_lib );
    isa_ok( $jb, $module );

    can_ok( $jb, 'library' );
    is( $jb->library(), $mock_lib,
        'library() should return library set in constructor' );

    can_ok( $jb, 'play_music' );

    $mock_lib->set_always( -path => 'my_path' );
    throws_ok { $jb->play_music( 'mp3', 'Romance Me' ) } qr/Unknown directory/,
        'play_music() should throw exception if it cannot find directory';

    $mock_lib->set_always( -find_dir => 'my_directory' );
    $mock_lib->set_always( read_file => { file => 'my_file' } );
    $mock_lib->set_true( 'save_file' );

    lives_ok { $jb->play_music( 'CD', 'Films For Radio' ) }
        '... but no exception if it can find it';

    $mock_lib->called_ok( 'read_file' );
    my ($method, $args) = $mock_lib->next_call( 2 );
    is( $method,    'save_file',       'play_music() should also save file' );
    is( $args->[1], 'my_directory',    '... saving to the proper directory' );
    is( $args->[2], 'Films For Radio', '... with the proper id'             );
    is( $args->[3]{count}, 1,          '... and the proper count'           );

Run the test with prove. All tests should pass.

What just happened?

The code for mocking objects should look familiar (see "Mocking Objects,” earlier in this chapter), but the code to create the mock object is different. In particular, this test loads the Library module and instantiates an actual object before passing it to the Test::MockObject::Extends constructor.

Note

Note which mocked methods the test logs and which methods it doesn’t. This is a useful technique when you want to test calls to some methods but not others.

Any methods called on the mock object that it doesn’t currently mock will pass through to the object being mocked. That is, without adding any other methods to it, calling save() or find_dir() on $mock_lib will actually call the real methods from Library. That’s why the first call to play_music() throws an exception: the directory name created in Library::find_dir() doesn’t exist.

The test then mocks find_dir() so that subsequent tests will pass. Next it mocks the read_file() and save_file() methods.

Because Library has put all of the actual file-handling code in three methods, it’s easy to test that Jukebox does the right thing without worrying about reading or writing files that may not exist or that the test may not have permission to access.

Note

When testing Music and its subclasses, it might be useful to mock play() too, depending on its implementation.

What about...

Q: How can you ensure that loading and saving work correctly?

A: That’s important, too, but that belongs in the tests for Library. This test exercises Jukebox; it interacts with Library only as far as Jukebox must use the Library interface appropriately.

Using mock objects is still somewhat fragile. In this example, if someone changes the interface of the methods in Library, the mock object may need to change. However, mocking only a few, small pieces of a well-designed object reduces the coupling between the mock object and the original object. This makes tests more robust.

Overriding Live Code

Plenty of useful modules do their work procedurally, without the modularity of functions and objects. Many modules, written before object orientation became popular, use package variables to control their behavior. To test your code fully, sometimes you have to reach inside those packages to change their variables. Tread lightly, though. Tricky testing code is harder to write and harder to debug.

How do I do that?

Suppose that you have a simple logging package. Its single subroutine, log_message(), takes a message and logs it to a filehandle. It also adds a time and date stamp to the start of the message and information about the function’s caller to the end, if two package global variables, $REPORT_TIME and $REPORT_CALLER, are true.

Save the following code to lib/Logger.pm:

    package Logger;

    use strict;

    our $OUTPUT_FH     = *STDERR;
    our $REPORT_TIME   = 1;
    our $REPORT_CALLER = 1;

    sub log_message
    {
        my ($package, $file, $line)  = caller();
        my $time                     = localtime();

        my $message                  = '';
        $message                    .= "[$time] " if $REPORT_TIME;
        $message                    .= shift;
        $message                    .= " from $package:$line in $file"
                                                  if $REPORT_CALLER;
        $message                    .= "
";

        write_message( $message );
    }

    sub write_message
    {
        my $message = shift;
        print $OUTPUT_FH $message;
    }

    1;

Fortunately, the module is simple enough, so it’s straightforward to test. The difficult part is figuring out how to capture the output from write_message(). You could test both functions at the same time, but it’s easier to test features in isolation, both to improve your test robustness and to reduce complications.

Save the following code to log_message.t:

    #!perl 

    use strict;
    use warnings;

    use lib 'lib';

    use Test::More tests => 6;
    use Test::MockModule;

    my $module = 'Logger';
    use_ok( $module ) or exit;

    can_ok( $module, 'log_message' );

    {
        local $Logger::REPORT_TIME   = 0;
        local $Logger::REPORT_CALLER = 0;

        my $message;
        my $logger = Test::MockModule->new( 'Logger' );
        $logger->mock( write_message => sub { $message = shift } );

        Logger::log_message( 'no decoration' );
        is( $message, "no decoration
",
            'log_message() should not add time or caller unless requested' );

        $Logger::REPORT_TIME   = 1;
        Logger::log_message( 'time only' );
        (my $time = localtime()) =~ s/:d+ /:\d+ /;
        like( $message, qr/^[$time] time only$/,
            '... adding time if requested' );

        $Logger::REPORT_CALLER = 1;
        my $line               = _ _LINE_ _ + 1;
        Logger::log_message( 'time and caller' );
        like( $message, qr/^[$time] time and caller from main:$line in $0$/,
            '... adding time and caller, if both requested' );

        $Logger::REPORT_TIME   = 0;
        $line                  = _ _LINE_ _ + 1;
        Logger::log_message( 'caller only' );
        like( $message, qr/^caller only from main:$line in $0$/,
            '... adding caller only if requested' );
    }

Run it with prove:

    $ prove log_message.t
    log_message....ok                                                            
    All tests successful.
    Files=1, Tests=6,  0 wallclock secs ( 0.10 cusr +  0.00 csys =  0.10 CPU)

What just happened?

The first interesting section of code, in the block following can_ok(), localizes the two package variables from Logger, $REPORT_TIME and $REPORT_CALLER.

Note

See “Temporary Values via local()” in perldoc perlsub for more details on localizing global symbols. This is a big topic related to Perl’s inner workings.

The benefit of local() is that it allows temporary values for global symbols, even those from other packages. Outside of that scope, the variables retain their previous values. Though it’s easy to assign to them without localizing them, it’s nicer to encapsulate those changes in a new scope and let Perl restore their old values. Inside the scope of the localized variables, the test uses Test::MockModule’s mock() method to install a temporary write_message() only for the duration of the lexical scope.

With the new write_message() temporarily in place, the message that log_message() creates will end up in the $message variable, which makes it easy to test the four possible combinations of reporting values. The rest of the code is straightforward, with two exceptions.

Note how the regular expression changes the output of localtime() to make the test less sensitive about timing issues; the test shouldn’t fail if it happens to run just at the boundary of a second. As it is, there is still a small race condition if the minute happens to turn over, but the potential for failure is much smaller now.

The other new piece is the use of the _ _LINE_ _ directive and the special variable $0 to verify that log_message() reports the proper calling line number and filename.

What about...

Q: What’s the best way to test write_message()?

A: write_message() performs two different potential actions. First, it writes to the STDERR filehandle by default. Second, it writes to the filehandle in $OUTPUT_FH if someone has set it. The Test::Output module from the CPAN is useful for both tests.

Save the following code to write_message.t:

    #!perl

    use strict;
    use warnings;

    use lib 'lib';

    use Test::More tests => 3;
    use Test::Output;
    use Test::Output::Tie;

    my $module = 'Logger';
    use_ok( $module ) or exit;

    stderr_is( sub { Logger::write_message( 'To STDERR!' ) }, 'To STDERR!',
        'write_message() should write to STDERR by default' );

    {
        local *Logger::OUTPUT_FH;

        my $out            = tie *Logger::OUTPUT_FH, 'Test::Output::Tie';
        $Logger::OUTPUT_FH = *Logger::OUTPUT_FH;

        Logger::write_message( 'To $out!' );
        is( $out->read(), 'To $out!', '... or to $OUTPUT_FH, if set' );
    }

Run it with prove:

    $ prove write_message.t
    write_message....ok                                                          
    All tests successful.
    Files=1, Tests=3,  0 wallclock secs ( 0.11 cusr +  0.00 csys =  0.11
        CPU)

Test::Output’s stderr_is() is handy for testing Logger’s default behavior. Its only quirk is that its first argument must be an anonymous subroutine. Otherwise, it’s as simple as can be.

Testing that write_message() prints to other filehandles is only slightly more complex. As with the tests for write_message(), the goal is to capture the output in a variable. Test::Output uses a module called Test::Output::Tie internally to do exactly that. It ties a filehandle that captures all data printed to it and returns this data when you call its read() method.

Note

Tying a variable with tie() is like subclassing a module; it presents the same interface but performs different behavior. See perldoc perltie to learn more.

Overriding Operators Everywhere

Overriding Perl operators locally is an important skill to know. Sometimes it’s not sufficient, though. Consider the case of code that calls exit() occasionally. That’s anathema to testing, but you don’t have to give up on unit testing altogether. If you can isolate the affected code to a few places in the program, you can test that code in isolation, redefining the systemwide exit() function to do what you want.

How do I do that?

Take the example of a module that enforces password protection for users. Save the following code as PasswordKeeper.pm in your library directory:

    package PasswordKeeper;

    sub new
    {
        my ($class, $username) = @_;
        my $password           = $class->encrypt( $username );
        bless
        {
            user     => $username,
            tries    => 0,
            password => $password,
        }, $class;
    }

    sub verify
    {
        my ($self, $guess) = @_;

        return 1 if $self->encrypt( $guess ) eq $self->{password};

        $self->{tries}++;
        exit if $self->{tries} =  = 3;

        return 0;
    }

    sub encrypt
    {
        my ($class, $password) = @_;
        return scalar reverse $password;
    }

    1;

Note

Don’t use this encryption technique for data you care about. See the Crypt namespace on the CPAN for better options.

That exit() looks a little dangerous, but at least it occurs in only one method. Save the following test file as pkeeper_exit.t:

    #!perl

    use strict;
    use warnings;

    use lib 'lib';

    use Test::More tests => 3;

    my $exited;
    BEGIN { *CORE::GLOBAL::exit = sub { $exited++ } };

    my $module = 'PasswordKeeper';
    use_ok( $module ) or die( "Could not load $module" );

    my $mel = $module->new( 'Melanie' );
    isa_ok( $mel, $module );

    $mel->verify( $_ ) for qw( buffy babycat milkyway );
    is( $exited, 1, 'verify() should exit if it receives three bad passwords' );

Note

Assume that another test file exercises PasswordKeeper’s non-exiting behavior.

Run it with prove:

    $ prove pkeeper_exit.t
    pkeeper_exit...ok
    All tests successful.
    Files=1, Tests=3,  0 wallclock secs ( 0.07 cusr +  0.02 csys =  0.09 CPU)

What just happened?

PasswordKeeper works by taking a username and encrypting it to make a password when it creates a new object. The verify() method takes a potential password, encrypts it, and compares it against the stored password. If they match, the method returns true. Otherwise, it increases a counter of failed attempts and exits the program if someone has tried three unsuccessful passwords.

Note

Read perldoc perlsub and perldoc perlvar to learn more about CORE::GLOBAL. This is very powerful, so use it with care.

That exiting is important behavior to test. The test file starts by defining exit() in the special CORE::GLOBAL namespace. That overrides exit() everywhere, not just in main, where the code of the test file lives, or in PasswordKeeper. The new exit() increments the $exited variable, so the third test in the file can check that PasswordKeeper called exit() once for three failed password attempts.

What about...

Q: What’s the advantage of overriding something everywhere instead of in a small scope?

A: You might not be able to localize all of the calls to exit() (or system(), die(), etc.) into one place in one module of the code you’re testing. In those situations, overriding the troublesome operator in a single test file that exercises the behavior can turn previously difficult code into testable code.

Make this test file small, so that it exercises only the code paths that cause the exiting. This will minimize the chances of unexpected behavior from your global overriding. If you can’t modify the code you’re testing to make it easier to test, at least you can encapsulate the tricky code into individual test files.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.98.148