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


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