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{}/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 => 'author@example.com',         recipients     => { Bob => 'bob@example.com' },     );     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 => 'alice@example.com' );     can_ok( $mt, 'recipients' );     is_deeply( $mt->recipients(  ),                { Alice => 'alice@example.com', Bob => 'bob@example.com' },                '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 => 'alice@example.com',         Bob   => 'bob@example.com',      );     while (my ($name, $address) = each %recipients)     {         my ($method, $args) = $smtp->next_call(  );         is( $method,    'mail',              'deliver(  ) should open a mail' );         is( $args->[1], 'author@example.com','... 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], 'author@example.com', '... 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.



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