In all of the debates over whether pair programming is exclusively 100% good or exclusively 100% evil or whether test-driven design is exclusively 100% beneficial or exclusively 100% silly, people sometimes miss the nuances of the polemic "if it's hard to test, it's hard to use".
In practice, that means that good programmers with good taste built from painful experiences have the ability to write better code if they exercise good taste when building tests.
(I know, this is the Internet of 21st century culture; the law of the excluded middle suggests that nuance, like irony, is deader than 19th century utopian cults. Doesn't mean that 10,000 volts of CPR are always wasted.)
That's what I had in mind when I wrote Mock Objects Despoil Your Tests. (See also Martin Fowler's Mocks Aren't Stubs.)
The more gyrations your code has to undergo before you're confident that it does what you intend it to do, no more and no less, the less confidence you have overall. In highfalutin' architecture astronaut terms, the more tightly coupled your tests are to the internals of your code, the worse your tests are. They could be fragile. They could make too many assumptions. They could be exercising things that no real code would exercise. They could be hard to write and overspecific.
In short, the likelihood that you've built yourself a maintenance burden is higher when you know far too much about the internals of a thing outside of that thing, even if the thing on the outside is a test intended to give you confidence.
(That's why I distrust putting code and tests in the same file, thank you very much Java. It's too tempting to cheat when the clear lines of demarcation aren't there.)
I only realized what I've been doing lately when I read Buddy Burden's Lazy == Cache?. He describes Moose lazy attributes the way I see them: as a promise to provide information when you need it. That laziness is a hallmark of Haskell. If you take laziness as far as Haskell does, you can build amazing things where things just happen when you need them.
Haskell, of course, goes a long way to encourage you to write programs in a pure style, where functions don't have side effects. Data comes into a function and data goes out, and the state of the world stays unchanged. Sure, you can't write any interesting program without at least performing IO, but Haskell encourages you to embrace purity as much as possible such that you minimize the places you update global state.
In my recent code, this has also just sort of happened, even in that code which isn't Haskell.
Consider an application which tracks daily stock market information, such as price and market capitalization. Each stock is a row in a table modeled by DBIx::Class. Each stock has an associated state, like "fetch daily price" or "write yearly free cash graph" or "invalid name; review".
No one would fault you for updating the stock price, market cap, and state on a successful fetch from the web service which provides this information. That's exactly what I used to do.
Now I don't.
I've separated the fetching of data from the parsing of data from the updating of data. Fetching and updating are solved problems; they happen at the boundaries of my code and I can only control so much about them. Either the database works or it doesn't. Either the remote web service is up or it isn't. (I still test them, but I've isolated them as much as possible.)
The interesting thing is always in the parsing and analysis. This is where
all of the assumptions appear. (Is Berkshire Hathaway's A class
BRK.a
or BRK-A
or something else? Are abbreviations
acceptable in sector and industry classifications?) This is where I want to
focus my testing—even my ad hoc testing, when I've found an assumption
but need to research what's gone wrong and why before I can formalize my
solution in test cases and code.
This means, the daily analysis method looks something like:
sub analyze_daily
{
my ($self, $stock, $updates) = @_;
my $stats = $self->get_daily_stats_for( $stock->symbol );
return unless $stats->{current_price};
$updates->{current_price} = $stats->{current_price};
return unless $stats->{market_capitalization};
$updates->{market_capitalization} = $stats->{market_capitalization};
$updates->{PK} = $stock->symbol;
return 1;
}
Any code that wants to test this can pass in a hash reference for
$updates
and a stock object (or equivalent) in $stock
and test that the results are sane by exploring the hash reference directly,
rather than poking around in $stock
.
(The data fetcher itself uses dependency injection and fixture data so that
all expected values are known values and that network errors or transient
failures don't affect this test; obviously other tests must verify that the
remote API behaves as expected. While I could make $stats
a
parameter here, I haven't had the need to go that far yet. There's a point
beyond removing dependencies from inside a discrete unit of code makes little
sense.)
This code is also much more reusable; it's trivial to create a bin/ or script/ directory full of little utilities which use the same API as the tests and help me debug or clean up or inspect all of this wonderful data.
Better yet, I find myself needing fewer tests, because each unit under test does less and has fewer loops and conditionals and edge cases. The problem becomes "What's the right fixture data to exercise the interesting behavior of this code?" My tests care less about managing the state of the objects and entities under test than they do about the transformations of data.
Perhaps it's not so strange that that's exactly what my programs care about too.