Beefy Boxes and Bandwidth Generously Provided by pair Networks
There's more than one way to do things
 
PerlMonks  

Unit Testing CGI Programs

by Wally Hartshorn (Friar)
on Mar 14, 2003 at 20:41 UTC ( #243173=perlquestion: print w/ replies, xml ) Need Help??
Wally Hartshorn has asked for the wisdom of the Perl Monks concerning the following question:

I'm attempting to Do The Right Thing (tm), i.e. unit test my programs. I've been reading about unit testing, and it sounds like a very good idea -- I'm sold on the concept! Unfortunately, I've been having problems figuring out how to do it. Nearly all of the programming I do involves database and CGI stuff.

Most of the information I've found about Test::Harness, Test::Simple, Test::More, etc, seem to focus on examples like this:

is ( add_numbers(2, 2), 4); is ( get_initials("John F. Kennedy"), "JFK");

I can understand how to write that kind of test, but I'm having trouble figuring out how to write a test for code that:

  1. Retrieves data from a web form (via CGI.pm).
  2. Retrieves the corresponding row from the database.
  3. Fills in the template (via HTML::Template).
  4. Returns the HTML output (for display by CGI::Application).

My suspicion is that I need to modify my program to make testing easier.

Suppose the original code is like this:

sub show_record { my $self = shift; my $cgi = $self->query(); my $emp_no = $cgi->param("emp_no"); ... # code to access the database omitted ... my $emp_rec = $sth->fetchrow_hashref; ... # code to fiddle with the data omitted ... my $template = $self->load_tmpl(emp_detail.tmpl.html'); $template->param( $emp_rec ); return $template->output; }

As it is now, I can't figure out how to write a test that would verify that the web page displayed for employee number "12345" is correct. How would I cause $cgi->param("emp_no") to return "12345"? How would I compare the web page to the expected value? When someone changes the format of the web page, the test would break. Etc.

As I thought about it, I realized that testing for the value of the web page would not be Doing The Right Thing. Instead, I should probably have a subroutine that gets the data ready for display, then test that routine instead. In other words, something more like this:

sub show_record { my $self = shift; my $cgi = $self->query(); my $emp_no = $cgi->param("emp_no"); my $emp_rec = get_emp_data($emp_no); my $template = $self->load_tmpl(emp_detail.tmpl.html'); $template->param( $emp_rec ); return $template->output; } sub get_emp_data { my ($emp_no) = (@_); ... # code to access the database omitted ... my $emp_rec = $sth->fetchrow_hashref; ... # code to fiddle with the data omitted ... return $emp_rec; }

With the above changes, I wouldn't do unit tests of show_record(), on the theory that it is pretty much all presentation code, not logic (although if anyone has suggestions on how to write unit tests for that, I'm interested). Instead, I would just test get_emp_rec().

Does that sound like Doing The Right Thing, or is there some nifty trick out there that I'm missing?

Wally Hartshorn

(Plug: Visit JavaJunkies, PerlMonks for Java)

Comment on Unit Testing CGI Programs
Select or Download Code
Re: Unit Testing CGI Programs
by adrianh (Chancellor) on Mar 14, 2003 at 21:12 UTC
    Does that sound like Doing The Right Thing, or is there some nifty trick out there that I'm missing?

    Pretty much the right thing. Code that is hard to test is often the better for a bit of refactoring. I think that you're right in your diagnosis. The problem you're having is due to having the presentation and logic too closely coupled. Separating it out into separate subroutines will help. Depending on the project it might be even more sensible to separate it out into a different class.

    You might find writing your code test first helps. It seems weird at first but, in my experience, improves code quality a great deal.

    As for testing show_template the technique you're after is mock objects. You make an object that pretends to be a template object, and then check that it was called with the appropriate parameters. Check out Automated software testing: emulation of interfaces using Test::MockObject and Micro Mocking: using local to help test subs for more info.

    You may also find Unit Testing Generated HTML, WWW::Mechanize, HTTP::WebTest, Test::HTML::Content and Test::HTML::Lint of interest.

Re: Unit Testing CGI Programs
by dws (Chancellor) on Mar 15, 2003 at 00:15 UTC
    Does that sound like Doing The Right Thing, or is there some nifty trick out there that I'm missing?

    The pieces you have can be unit tested individually if you break them out. Philosophically, you're doing three things:

    • getting a key from a form (trivial to test in isolation)
    • issuing a query based on that key (easy to test in isolation)
    • expanding the value(s) returned from the query into a template (easy to test in isolation)
    You've made a start on teasing these apart into separately testable routines. One possible next step is to isolate the logic that constructs the template. With the logic broken into separate routines, you've in a much better position to selectively replace substructure for testing purposes. (E.g., pull data back from in in-memory stub or repository, rather than querying the database).

    On the "trick" side, here's one I've gotten mileage out of: Instead of hard-coding the template name, soft code it in a way that your test harness can change it to use a debugging template. The debugging template strips out everything unnecessary, leaving you with a result (and expanded template) that's easy to verify programmatically.

(dkubb) Re: (1) Unit Testing CGI Programs
by dkubb (Deacon) on Mar 15, 2003 at 06:18 UTC

    I agree completely with others in this thread who have suggested that you should break your application into components that can be more easily tested separately from the rest of the app.

    When I develop CGI apps, or mod_perl handlers, I usually use a combination of Test::More and Apache::test (included with mod_perl), along with a set of other testing modules like Test::Manifest, Test::Distribution and Test::Prereq.

    Apache::test allows me to start up my own private Apache web server, listening on a non-standard port, where I can perform LWP requests against it and check for expected output. As you've already mentioned this technique definately has some limitations, such as if the HTML changes, then the tests break. I try to get around this problem by doing the following:

    1. All code is version controlled. I use CVS mainly, but I may be moving to Subversion shortly.
    2. All new code is bundled in small packages that are interrelated, and all the support files in one place (HTML templates, test files, installation scripts). I've modelled this after the way CPAN modules are bundled, and it works amazingly well.
    3. Things like template names, database connection information, etc are pulled from external configuration sources. This might be an httpd.conf if I'm writing a mod_perl handler, or it could be a flat file/database if I'm writing a CGI.

    The theme here is that I'm trying to make the testing environment as controllable as possible and reducing unknown factors.

    I use version control so I can tag a group of files as being "stable", which means "if all of these files are checked out with this tag, the test cases should pass".

    Keeping everything in self contained CPAN-style bundles and using Makefile.PL's gives me everything I need to check dependancies, run the unit test cases, and install the applications. I'd recommend looking into Module::Build if you're writing new code as a replacement for Makefile.PL and ExtUtils::MakeMaker -- I've been meaning to switch to it for some time, but haven't yet had a chance to learn its ins and outs.

    By putting the template paths in external configuration, I can tell the application to use my simple pre-formatted HTML page, with all layout elements removed. The important thing is that I control the layout of this page. This allows me to write LWP scripts that can work with the application, parse its output, and check this against expected output. Side Note: depending on your application you may even be able to swap in DBD::CSV for your native database, thereby removing the application's dependancy on an external DB - I've only done this for smaller-ish applications, so some experimentation is probably necessary.

    The downsides? Apache::test can be tricky to get working, but once you do it can be pretty easy to duplicate later on. The first few times you do this it will feel like you've taken on alot of extra overhead, all I can say It will pass with a few repetitions. Another thing is that this may be overkill for some applications, you'll have to decide if its appropriate or not. And finally you might be tempted to skip over conventional unit testing because you think this is enough. Don't, its not. This approach is complementary to doing unit testing, not a replacement for it.

    Dan Kubb, Perl Programmer

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://243173]
Approved by Enlil
Front-paged by diotalevi
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others pondering the Monastery: (6)
As of 2014-07-30 01:30 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    My favorite superfluous repetitious redundant duplicative phrase is:









    Results (229 votes), past polls