Part of 9 in the series
Today, let’s talk a little about the chaining premise, from five underplayed tdd premises.
The chaining premise says "test a chain by testing its links". Like the other premises, it’s easy to make it pithy, but it has vast ramifications about when we’re doing TDD.
When we talked about the money premise, I gave a long, likely partial, list of ways TDD supports that premise. Did you notice I never mentioned the customer? TDD is for developers. The people it helps the most are the geeks who do it. (oh, don’t worry. TDD works for developers, but remember, developers work for managers, and managers work for customers. It all works out in the end, I promise.) for the TDD tests to make us ship more value faster, they have to be "cheap". I’ll define that vague word using some other vague words. "cheap" means "easy-to". TDD tests want to be easy to … Scan, read, write, change, run, and diagnose. That means — remember the thinking knee — they have to be small.
Consider an app. It consists of, in decreasing order of size: systems, subsystems, programs, layers, packages, objects, and functions. All arranged in an intricate directed dependency graph. A calls B calls C calls D calls, well, it’s dependencies all the way down. Some apps are huge. Most apps are just "bigger than a breadbox". A small number of apps are small.
How are we going to write small tests against large apps? This is where the chaining premise steps in.
The chaining premise says 1) we can test by testing only parts of the app at one time, and 2) the natural parts are the arrows in that A->B->C->D dependency chain, and 3) the cheapest tests work pair-by-pair along the chain. A possible confusion: it’s not really a chain. Dependency graphs are directed graphs, and any given part might depend on more than one other part. Normally, A willl depend on B and C, each of which have their own dependencies.
Call a "unit under test" one of these letters, A. Say that A depends on, uses, imports, B, C, and D. We’ll say that A "collaborates" with B, C, and D, and that they are A’s "collaborators". To test that A does what the geek thinks it does — the heart of TDD — we hook A up to its collaborators, give it some commands, and poke around a little to see if A did the thing we thought it did.
Okay, but wait. Isn’t it true that if I hook A up to a collaborator, i’m hooking it up to all of that collaborator’s dependencies, and so and so on all the way down?
Fine. We’ll trick the A. We’ll give it a thing that it thinks is its collaborator, but is really just a simulator!! MY GOD, WE’RE GONNA BE RICH!!! Except. Wait. If you can write a simulator for a collaborator that does everything that collaborator does in every possible circumstance, aren’t you going to wind up with just as many dependencies?
Are we stumped? Is the chain premise now dead? Sure feels like a lot of damned thinking for nothing. Ya know, mama dint raise no thinkers.
So ya got these tests and you need — oh. That’s it, that’s the problem, we’re thinking of all these tests. But we only actually write, or for that matter scan/read/run/change/diagnose one test at a time. We don’t need a big heavy simulator, we need a bunch of really stupid fakes. Really stupid fakes are much easier than simulators. If we can find ways to do this quickly and easily, we’re back on track.
So this chain premise has us testing A by testing one path through one function at a time, supplying really stupid fakes instead of real collaborators.
A contrived but demonstrative example. In order to decide what to do with a notification, say that A needs to know whether it’s business hours or not. It uses an Hours for this. It says "if(hours.isCurrentlyOpen()) … Else …" we write one test for the if, and another one for the else. We don’t use a real Hours for either test. For the first one, we have an Hours that always says it’s currently open. For the other, the opposite.
Notice that this trick breaks the dependency tree. A still depends on Hours, but the Hours it depends on doesn’t depend on anything else. This means I am testing just one piece of the chain, the A. I’m not testing its collaborators, i’m not testing the app, i’m testing A.
And what, after all, are we testing here? We are testing that A works the way we thought it did ASSUMING that its collaborators work the way we thought they did. A person might wonder how much that’s worth. How much is it worth to know that a given piece works assuming the pieces it talks to work? The answer is that it’s worth way more than the cost of doing it.
Or, anyway, that’s what the chain premise says. "test a chain by testing each link". If we can cheaply know for every A, B, C, … That it works if what it depends on works, I have gone a fabulous distance towards knowing that the whole thing works.
Time for some provisos and caveats.
- We get to choose when and where we break the dependency chain for testing. There are lots of places where it isn’t worth doing. One never writes a fake String or Integer, or instance. Using real ones works just fine.
- There are lots of ways to do faking, of various weights and sophistications. You will find mucho de argumento about which one represents the One True Way[tm]. Know this, tho, any turing-complete environment can do this.
- Practice practice practice. Doing this, seeing this, arranging things so it works, this is not an instantly learned skill. I’d hazard most of your TDD studs spent 2-5 *years* getting good at it. We can speed you along a little, but it’s never going to be instant.
- MOST IMPORTANTLY: this really only works *cheaply* if we build it that way. There are lots of dependency trees that would make it very costly. This is the steering premise, which we’ll do in depth real soon now.
The chain premise says "test a chain by testing each link". We test a link by pulling it out of the chain, testing it in various degrees of isolation. The knack and difficulty of TDD is in how and when we do this.