Software Programs can be understood as (potentially huge) orchestras playing in concert.
Depending on your level of abstraction, you might imagine systems, subsystems, layers, packages, objects, or even functions as the individual players.
(Aside: Folks often make major distinctions in these abstractions, but to my touch, they feel all the same thing, just with ever larger labels on ever subsets. and at bottom, 100% of it is Von Neumann architecture code.)
For the purposes of this conversation, I’ll be thinking and using classes and objects, as that is the main level at which daily geekery is conducted in the modern synthesis. But remember, the players could be any of these.
The players collaborate to perform their function. They are collaborators. We often speak of roles or responsibilities – sometimes very formally – and that language is pointing at the same thing. Parts of programs work together: They collaborate. I pretty much always use TDD for anything bigger than the very rare "three hours, ship it, never look at it again" thing. I won’t rehearse the reasons for this, but just offer my official view: it lets me ship more value faster when I work that way.
Anyway, when TDD’ing, all collaborators are decidedly not created equal, and they’re unequal in a peculiar way. Let me throw a strange pair of terms at you: "graceful" and "awkward". It’s not an all-or-none thing – but one of continuum, with some collaborators being very graceful and some being very awkward, and plenty in between, and some awkward sometimes and not others. TDD works for me because the tests are cheap. We’ve used this word before, and I used the phrase "easy to". Easy to write, to read, to scan, to run, to change, to collect, to debug.
The idea is really important, "cheapness", for a very straightforward and human reason: when things aren’t cheap, we don’t do as much of them. If the things have substantial extrinsic value, not doing as much of them is A Bad Thing[tm]. When I’m adding new functionality, my new stuff is usually merrily collaborating away, with my code, her code, library code, framework code, etc. etc. A graceful collaborator is one that makes or keeps my testing of the new functionality cheap. An awkward collaborator is one that prevents my testing from being cheap. (Remember as we go, continuum not binary value.)
The archetypal graceful collaborator in most modern OO languages is the string class. Many TDD’ers write their very first tests on code that manipulates strings. And the tests are chock-a-block with assertions that two strings do or don’t match. Awkward collaborators aren’t always the same from one programming environment to another. In java, the simplest awkward is usually the File class. In other worlds it might be different. A sampling of common awkwards: A ticks-since-1970 clock. A physical file, a relational database. Fronts for external devices, like screens or printers. Fronts for other complex programs, servers or report-writers.
What makes graceful "graceful" and awkward "awkward"? Well. Anything that makes me not want to use a test to validate that my code does what I meant it to contributes to awkwardness. And graceful is the absence of any of those things. Awkwardness can be in runtime, it can be in setup cost, it can be in difficulty obtaining the output data, it can be almost anywhere. In our material at Industrial Logic (cites at end), we just say "if it makes you not want to test it, call it awkward and do something about it."
Strings are relatively graceful, because they require one-liner setup, one-liner probe, they have no externalities, they run fast, and for all but the noobs, their API is incredibly well-understood.
(Note: Don’t make the mistake of thinking that this is because strings are simple or have no dependencies. try learning every single corner of, say, c++ std::string. pro-tip: bring food, as you won’t be done before lunch — next week.)
A substantial part of doing TDD in the real-world hinges on this awkward/graceful distinction. In fact, the reason so many toy TDD exercises are in fact toys, is because they include few or no awkwards, where real-world TDD is replete with them. How one handles awkward collaboration both potential and pre-made, is at the heart of successfully using TDD to ship more value faster.
There are several different ways to address awkward collaboration, and no single one ring to rule all awkwards. About all I can do is offer some generic advice, for the moment:
- We struggle mightily to keep the current class — the new functionality — from becoming the newest awkward collaborator in our collection.
- We can very often divide the work our code does into an awkward part and a graceful part. Sometimes that’s as easy as splitting one method into two. The first method turns awkward into graceful, the second method takes graceful.
- We nearly always use interfaces when an implementation is inherently awkward. Big girls and boys supply a graceful (if usually incomplete or simplified) variant of an awkward FOR NO OTHER REASON than to keep from passing on the pain.
- Alternate constructors — one taking an awkward, one taking a graceful, are extremely common.
- Indirection is your friend in that 95% of your job that has almost no impact on program performance.
- The most important part of your code is usually not the bringing together of the data, but the processing of it. If we intermingle those modes, we often create permanent awkwardness.
- Often, the many paths through my code all have a prolog or epilog that’s awkward, with pure grace in the middle. If I tie those into the code in a non-separable way, I’m locking everyone into my awkwardness for all time.
- The smaller an awkward’s interface, the cheaper it is to fake. (Faking has lots of names, most commonly mocking, a misuse of that term’s origin and intent. It is making an artificial "test only" graceful from some inherently awkward thing.)
Anyway, it’s getting on toward bedtime, and that list is weird and disjoint, so I’m going to leave it for now. Maybe just start here: Learn to recognize the feeling that you don’t want to roll a test because it’s not cheap enough to do so. Think about what could be changed — your code or others — that might ease that pain.