Today I came across some undertested code, and Allison and I had a short discussion about the best way to approach it.
If you've read my You're Already Using Dependency Injection, you may approach the problem of testing from the mindset that looks for dependencies—declared and undeclared—and tries to manage them. That's essentially the design goal of automated testing, or at least test-driven development. We want to produce a single system with sufficient decoupling that we can prove small, unique, and isolated assertions about the behavior of our code.
Sometimes Perl makes that easy for us. Sometimes it doesn't.
Good programmers use Perl modules to encapsulate discrete units of behavior. Yet if we're not careful, we can limit our options. Consider this code:
package Project::DB::Connection;
use strict;
use warnings;
use DBI;
use Project::Config;
my $dbh = DBI->connect( Project::Config->get_config( 'database' ) );
...
The desire to make $dbh
a singleton is understandable, as is
the desire to encapsulate it as a file-scoped lexical. (Assume there's a
get_dbh()
function exported or available.) Those are very likely
advantages.
You can't immediately see the disadvantages, however.
If something else in your system use
s this module, Perl puts an
implicit BEGIN
around all of the code. That means before the
use
statement has finished executing, this code will already have
run. $dbh
will be connected.
As well, this code has a dependency on the configuration module. Presumably that gets loaded too, and it may itself run code.
If you need to override any part of the database connection (to use a separate database for testing, if you don't necessarily have access to the correct database on a testing machine, or for whatever reason), you have to hijack something and you have to make that happen before this code runs.
Deciding out what to hijack (do you mock the DBI? override part of the
configuration? override get_dbh()
and hope the initial connection
works well enough in your testing environment?) is sometimes easier than
figuring out what loads what else and when so that you can get the implicit
order of loading correct. Perhaps even worse, you have to write really clever code to get Perl to do the right things in the right ways in the proper order. (Perl lets you do this, but just because you can do something doesn't make it a good idea.)
Compare that to the dependency management of lazy object attributes as exemplified by Moose.
Again, this may not be important to you until you want your test suite to run in an environment not exactly like the one you're using at the moment. (Shortcuts have a way of coming back to haunt you.) Even if you're perfectly happy in a simple environment you can't reproduce with the push of a button, the day may come when you want to know if your code works anywhere but your development machine.
That's when the degree to which you managed your implicit dependencies will either help or hinder you.
(That's why I make a habit of not running any code in implicit
BEGIN
blocks; it's cost me rework too many times.)
You showed the problem in a very clear way. Could you also provide a code snippet for a solution (db handle singleton, dependency to many configuration items) which seems "better" in your opinion?
Best regards
McA