Part of 9 in the series
Time, finally, for the steering premise, from the five underplayed TDD premises.
The steering premise says "tests & testability help steer design & development". What we’re saying here is that tests are first-class citizens in the mob of factors that shape our system, with a voice that counts, all the way through development.
Think of the factors we take in to account when we make software. They range all over, from market considerations, to platform, from our geek skillset to our tech stack. We operate within this mob of factors. We decide what to write, when to write, how to write. And the factors contribute their voices all along. The steering premise says that tests & testability are as central and important as each of the others.
It might be rhetorical to class this with the underplayed. After all, the movement is test-DRIVEN-development. A thousand heartaches ago, when TDD first hit these mean streets, the most shocking thing about it was that we wrote a test before we wrote the code to pass it. It was often called tFd. (no, not that F: "first". "test-FIRST-development". Get yer head outta the gutter.) writing a test before you change the code to pass that test, this is the steering premise at its most micro most rigorous level. And when geeks like me tried to adhere to this most rigorous advice, we discovered several remarkable things.
You can’t write tests before code very many times without discovering that some tests are dead easy to write, and some tests are hard as hell. And you think it’s cuz you’re a noob and you’re full of vigor, so you keep at it. And you eventually make two discoveries or give up TDD.
- Discovery #1: hard testing problems are patterned. That is, they resemble each other in detectable and describable ways. “the database needs loading.” “the threads have to context-switch.” “the pixels can’t be interrogated.” “the answer is stochastic.”
- Discovery #2: hard testing problems can very often be turned into easy testing problems by rearranging where code lives and how it’s called.
And once we’ve made these discoveries, the steering premise goes from its micro scope to the macro. Why? Well. We’re not stupid. If we can rearrange things to make hard tests easy tests, why not arrange things more nearly towards easy in the first place?
A real, if stupid, example. Spoze we get a filename, and we have to get a hundred comma-delimited values out of that file.by way of psuedo-code, i’ll show you the paragraphing comments the noob types in.
// make file from filename // make stream from file // parse stream
Now if we put all this in one method. To test that method, we’re going to have to litter the arena with sample files. (normally, we stash all these in some /test/ hierarchy). The parse might need a dozen examples to satisfy us that it works, so that’s a dozen files. So we make all the files, and we write one, then we copy/paste/edit the others. And as the code develops we do more and more of this, and we cook up an ingenious naming scheme for them.
And because you can’t "de-duplicate" files, if we ever change the comma delimiting to tab delimiting — see "why developers hate everyone" — we go back and manually edit it all. The point is, this is hard. It’s not a cheap set of tests to scan/read/write/run/change/diagnose.
A slight rearrangement, the slightest, will greatly ease our pain. Put the first two lines, whose effect is to go from filename to stream, in one method. Put the third line in another method.
Testing the first method is still tedious, cuz we have to have some files to test it on. But a) there are fewer cases, having to do with file-nature, not file-content. B) in real-life we’d prolly elide the test altogether, as it’s a sequential series of pure library calls.
Testing the second method can now be done by passing in streams. Streams are far easier to make and organize and edit and de-duplicate than files, because they can be made with one line of code and a string constant. (as I say, this case is dumb, the answer is easy, the rearrangement trivial. But this is real. I’ve been in shops all around the world that use files in tests when they need not do so.)
Do you see what we did? We changed our design to make it easier to test. We steered.
"I’ll be damned," you say, "So we did. We changed our design to make it easier to test. Damnedest thing I ever saw." well. You’re kinda impressionable. But the truth is, it is the damnedest thing, for a slew of reasons.
There are lots of these cases, where we can rearrange things to make testing cheap. They form the set of patterns that experienced TDD’ers carry around with them.some of the rearrangements are easy to learn, others harder. The answers don’t usually just drop in effortlessly, but require custom fit, so they’re true patterns.
They do nearly always fit a meta-pattern: "eliminate or mitigate awkward collaborations". That’s a muse for another day.
Our case here is low-level code. To take the steering premise to its full extent, tho, we need to grow both "low-level" and "code" in our vision."low-level" suggests that this kind of thing only applies to, idunno, casting about for a phrase, "leaf nodes": close to the bottom code chunks, as opposed to "big picture abstractions". This would be mistaking one case for the range of cases.
A lot of folks layer their thinking — I don’t, and am generally opposed to too much of this, but I know what they mean — they think there’s "code", "design", "architecture", in ascending levels of abstraction.
The more complete version of the steering premise aims at the entire pyramid of abstractions. It’s not just about the breakout of functions, it’s about functions, objects, layers, subsystems, programs, systems, and apps. That is, refactoring — that’s what we did in that simple code case — can be done at any level of the abstraction hierarchy. In fact, it’s often easier to make tests cheap well above the level of leaf nodes.
We grow our vision of steering vertically by saying that we steer all the levels, from highest to lowest, by taking tests & testability seriously as factors.
And what about "code"? Does the steering premise only apply when we’re looking at structured UTF-8 text that’s to be executed by a computer? No. We can incorporate tests & testability as factors not just in the code, but all through the process of shipping more value faster. Making tests & testability first-class factors let’s us move out from code-per-se to coding-per-se, and from there to shipping-per-se.
Consider that TDD enables continuous integration (CI) and that in turn enables continuous deployment (CD), and that has huge impact on our customers and our market. I think of this as expanding steering horizontally. The steering premise reaches far beyond the scope of "arranging structured text to be exceuted by von neumann architecture devices". It doesn’t just change how we see code, it changes how we see the whole activity-set.
The steering premise says we treat tests & testability as first-class citizens throughout the entire range of the software development game. It is at the very heart of TDD, and everything TDD’ers do depends on and draws from it.