Beefy Boxes and Bandwidth Generously Provided by pair Networks
Problems? Is your data what you think it is?
 
PerlMonks  

Automated software testing: emulation of interfaces using Test::MockObject

by IlyaM (Parson)
on Jun 13, 2002 at 00:26 UTC ( [id://174082]=perlmeditation: print w/replies, xml ) Need Help??

Intro

Warning: if you have never wrote automated tests for your code, if you have no idea what about it and if you do not know why you want to write them then please read first An Introduction to Testing.

It is easy to write tests for code which doesn't have dependancies on external environment. When you write tests, say, for implementation of some mathematical algorithm you have easily controlled environment. Everything you have to worry about is supplying input values and comparing them with output. But how can you test code which, say, sends emails using Net::SMTP module? To satisfy requirement for controlled environment you will have to run test mail server which must be controlled by test code.

Often very simple technique of using fake intefaces may be much more easier alternative. Instead of running test mail server emulate Net::SMTP interface in test code. Emulation may actually do not send emails over network but pass information about sent emails to testing code.

I'll try to show this technique on example of Perl module which uses Apache API (so it expects mod_perl environment) and I'll show how it can be tested using emulated mod_perl environment.

Example Module

Example Perl module extends CGI::Cookie module. It's POD documentation should be self-explanatory.

=head1 NAME Apache::Perl::Cookie - interface to Netscape Cookies =head1 SYNOPSIS use Apache::Perl::Cookie; my %cookies = Apache::Perl::Cookie->fetch; $cookie = Apache::Perl::Cookie->new(-name => 'ID', -value => 1234); $cookie->bake; =head1 DESCRIPTION Apache::Perl::Cookie is subclass of L<CGI::Cookie|CGI::Cookie>. It uses Apache request object instead of enviroment variables to get existing cookies. Also it adds method for sending cookies using Apache request object. Note that this module serves as example only. In real mod_perl programs you more likely to find L<Apache::Cookie|Apache::Cookie> module useful. =head1 BUGS AND LIMITATIONS This module relies on mod_perl API so it cannot be used in non-mod_perl environment. =head1 SEE ALSO L<Apache|Apache> L<Apache::Cookie|Apache::Cookie> L<CGI::Cookie|CGI::Cookie> =cut package Apache::Perl::Cookie; use strict; use warnings; use base qw(CGI::Cookie); sub fetch { my $class = shift; my $header = Apache->request->header_in('Cookie'); if(defined $header) { return CGI::Cookie->parse($header); } return; } sub bake { my $self = shift; Apache->request->headers_out->add('Set-Cookie' => $self); } 1;

Test Script

As POD documentation says this module relies on mod_perl API. So how are we going to write tests for it without using Apache configured with mod_perl support?

It is much more easier to fake mod_perl environement instead of using Apache for automated tests. There is no need to emulate all mod_perl API. It is sufficient to emulate only parts of it which are used in the module. Apache emulation interface can be implemented using fake Apache module. I.e:

package Apache; sub request { my $class = shift; return bless {}, $class; } sub header_in { .... .... ....

But I'll use another method. One very useful module for building inteface emulations is Test::MockObject. Here example of test script which uses this module to fake mod_perl environment:

use Test::More tests => 9; use strict; use warnings; use Test::MockObject; # build mod_perl interface emulation my $COOKIE_HEADER_IN; my $COOKIE_HEADER_OUT; { my $headers_out = Test::MockObject->new; my $request = Test::MockObject->new; # Create 'request' method in package Apache which always returns # same fake request object $request. Test::MockObject->fake_module('Apache', request => sub { $request }); # Add 'header_in' method which returns value of $COOKIE_HEADER_IN # variable when asked for 'Cookie' header. $request->mock('header_in', sub { my ($self, $name) = @_; return $COOKIE_HEADER_IN if $name eq 'Cookie'; return; }); # Add 'header_out' method which always return fake Apache::Table # object $headers_out. $request->set_always('headers_out', $headers_out); # Add 'add' method which stores passed header value in # $COOKIE_HEADER_OUT variable for 'Set-Cookie' header. $headers_out->mock('add', sub { my ($self, $name, $value) = @_; return unless $name eq 'Set-Cookie'; $COOKIE_HEADER_OUT = $value; }); } # test if we can load module require_ok 'Apache::Perl::Cookie'; { # scenario #1 - no cookie header $COOKIE_HEADER_IN = undef; my %cookies = Apache::Perl::Cookie->fetch; is(scalar(keys %cookies), 0, 'No cookie header - no cookies are expected'); } { # scenario #2 - empty cookie header $COOKIE_HEADER_IN = ''; my %cookies = Apache::Perl::Cookie->fetch; is(scalar(keys %cookies), 0, 'Empty cookie header - no cookies are expected'); } { # scenario #3 - cookie header with one cookie $COOKIE_HEADER_IN = 'Name=Value'; my %cookies = Apache::Perl::Cookie->fetch; is(scalar(keys %cookies), 1, 'One cookie is expected'); is($cookies{Name}->value, 'Value'); } { # scenario #4 - cookie header with two cookies $COOKIE_HEADER_IN = 'Name1=Value1; Name2=Value2'; my %cookies = Apache::Perl::Cookie->fetch; is(scalar(keys %cookies), 2, 'Two cookies are expected'); is($cookies{Name1}->value, 'Value1'); is($cookies{Name2}->value, 'Value2'); } { # scenario #5 - send cookie my $cookie = Apache::Perl::Cookie->new(-name => 'ID', -value => 1234); $cookie->bake; is($COOKIE_HEADER_OUT, 'ID=1234; path=/', 'Test if cookie have been sent correctly'); }

Conclusion

Fake interfaces is great technique which simplifies task of creation controlled environments for automated tests. Once you learned it you have less reasons to avoid writting them :)

Update: Recent versions of Test::MockObject deprecate usage of 'add' method and suggest to use 'mock' method instead of it. Tutorial have been updated to reflect recommended usage.

--
Ilya Martynov (http://martynov.org/)

Replies are listed 'Best First'.
Re: Automated software testing: emulation of interfaces using Test::MockObject
by Ovid (Cardinal) on Jun 13, 2002 at 14:40 UTC

    Nice introduction. Test::MockObject, by chromatic, is a great module. I'd just like to add that it pays to understand where and when it's important. For example:

    package Baz; use Foo::Bar; my $stuff = Foo::Bar->new( $things ); # do a bunch of things that we want to test

    In that example, you're creating a module "Baz", and you want to use Test::MockObject to dummy up a Foo::Bar object. What do you do, though, if $things is not a valid argument to Foo::Bar? There's a good chance that your mock object won't catch this (I've been bitten by this).

    Of course, this testing module isn't designed to catch things like that because it's not supposed to. In fact, you could leave the use statement and the constructor out of there and if the rest of the module depends on that object, the mock object may very well still allow the module to work.

    Once you have the basic functioning of your code working with this module, then you need to run integration tests without it. Test::MockObject can actually obscure many integration bugs if you're not careful. However, since this module was specifically designed for unit testing, that's not to be taken as criticism. All in all, I give it two thumbs up.

    Cheers,
    Ovid

    Join the Perlmonks Setiathome Group or just click on the the link and check out our stats.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://174082]
Approved by lemming
Front-paged by ignatz
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others admiring the Monastery: (7)
As of 2024-03-19 02:01 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found