An important steerability force for me is in my strategies for sidestepping the pyramid problem.
There are a lot of ways we could describe this "pyramid problem". Perhaps the most straightforward way is to ask how much code is "in play" during a test. At some point, and it varies by codebase, the answer becomes "a lot". When it does, you’re encountering the pyramid problem.
I call it the pyramid problem because I see code as great big pyramid-shaped piles of dependencies. At the top is "the app". At the bottom is "the primitives". In between, we have (ideally) levels that "fan-out" in a self-similar fractally way.
If you like, you can call it the "stack problem", that’s fine, too. What we’re talking about, really, is that starter question: "how much code is in play during this test?" And here’s the thing, though there are exceptions, the general rule is this: the more code that’s in play, the more expensive the tests for that code.
Remember, TDD isn’t a religious exercise in intellectual purity, we’re in this for the money — for shipping more value faster. The cost of the tests is a critical factor in the value of TDD. As the cost of the tests rises, their value to me drops. And the relationship isn’t usually linear. At some point, that cost flips TDD’s value proposition entirely: far from making me go faster, TDD makes me go slower.
(This, btw, is why some shops have "tried" a what they call "TDD" and now hate everyone and everything associated with it. Personally, I blame the pedagogy and the ridiculously thin culture of geekery for this, and my hobby is fixing it, partly by doing what I’m doing right now.)
Tests that invoke large masses of code bring a variety of expense. They bring, for instance, slow runtime, reading & writing & scanning difficulty, debugging pain, combinatoric pain, and so on.
There are lots of tactics that can help with the pyramid problem, but the strategy comes down to this: break the dependency tree into subtrees, test them separately, and test the higher levels without using the lower ones, by either faking or ignoring them.
This almost always means re-arranging a "naive" dependency tree in clever ways. That is why we make sure tests and testability are first-class citizens in design, because without that force, we’ll recreate the pyramid problem over and over again.
A simple example will maybe help. When you write code that connects to a database, you form up SQL statements using strings, and you send them off to the database server. In a naive design of an app that does a lot of that kinda crap, you’ll have these SQL-making elements spread out all over hell’s half-acre. Every place you need something from the database, you make a SQL statement, and, RIGHT ON THE SPOT, send it into the ether.
The impact of arranging things that way is enormous: it means every test you write against that code becomes more expensive. In most cases, tho not all, dramatically more expensive.
A TDD’er will glance at this and shudder. She’ll say no, we have to tweak this so that it’s testable more cheaply. And she’ll re-arrange the dependency tree in one of a couple of ways that will do just that.
One possible way: make a new class that exposes the results of such a SQL call, a list or something more domain-specific, rather than the call itself. Make our old code take an instance of that. Test it by passing in instances we’ve hand-loaded. Now it’s testable.
(There’s other ways, and in some cases better ones. This is all just for illustration purposes.)
I know a lot of you are reaching for your auto-mocker about now. I’ve said elsewhere, and will say again, an auto-mocker is not a great tool to put at the front of your belt. I offered this variant of the answer precisely because it doesn’t involve any faking at all.
The pyramid problem is universal. That is, all serious apps these days are written using dependency trees with thousands of nodes in them.
And for any such pile of dependencies, there are ways to arrange them that are "more testable" and "less testable". Where any particular arrangement sits on that spectrum is of vital importance to as designers — as arrangers of such trees. The big force that steerability brings into my design considerations is this: how can I arrange this pile so that I can test it rigorusly enough that I trust it.
As I say, there are a bunch of tactics, ranging from swapping supplier and supplies, to extract & override, to full-on faking and even to mocking. But the goal’s the same every time, and so is the strategy.
Break big trees into subtrees. Test the subtrees. Test the higher levels by either faking or ignoring the lower ones.
Do have a lovely Saturday night, folks. I’ll do my best to do the same. 🙂