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$uri\E/;             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.



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