http://qs321.pair.com?node_id=174082

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/)