How testability can help

In David Heinemeier Hansson’s RailsConf 2014 keynote and accompanying blog post, he criticises “testability” as a meaningless goal. His argument characterises TDD practitioners as being fixated on testing qua testing, driven to pursue testability and associated metrics (coverage, ratio, speed) in their own right, as if they unquestioningly believe those things to have intrinsic value.

A slide from DHH’s keynote, showing the words "Coverage, Ratio & Speed" superimposed on an image of the Holy Grail

That’s inaccurate and unfair. Testability is useful because it’s an effective, tangible proxy for other properties of software that are harder to recognise: modularity, composability, reusability and so on.

Tests are useful on many levels, but at their most basic they provide a second client for your implementation, encouraging you to think harder about what each part of your software is doing and how it’s doing it. They give you an opportunity to step outside of your immediate goal and look at your software in a different way, from a different angle, with a different set of priorities. This gives you more visibility on the decisions you’re making, and that’s almost always worthwhile.

If it’s a nightmare to isolate a piece of your implementation in order to test it, what does that tell you? Directly: it’s not very “testable”. Indirectly: your design is perhaps a bit tangled up, and you probably could work harder to separate concerns and isolate dependencies and think carefully about how the pieces interact and what they individually mean, and it might be difficult to compose those pieces in different ways later. Would you have noticed those problems anyway? Probably, but going through the exercise of writing tests is one way of increasing the chances of noticing them sooner, while you still have a chance to do something about them before they become too baked-in.

To pick a tiny, arbitrary example from the keynote, DHH criticises this method…

def age(now = Date.today)
  now.year - birthday.year
end

…in contrast to his preferred version…

def age
  Date.today.year - birthday.year
end

(Both implementations are wrong, but that’s beside the point.)

“Is [the method with the now parameter] better? Is it simpler? Is it clearer?” He makes fun of it as though the latter version is obviously simpler and clearer — presumably because it uses fewer characters, parameters or concepts — but in reality it’s not obvious. It depends what you want!

The first version makes it “clearer” that the method’s result is date-dependent, which makes it “simpler” to understand how it will behave as part of a larger system (e.g. is it cacheable?); this is a win for composability. If you want to call #age from inside another method, you’ll need to get a Date from somewhere — maybe you’ll already have an appropriate one to hand, or maybe you’ll choose to pass that responsibility onto your caller in turn, or maybe you’ll decide that this is the right place to reach for Date.today. Whatever choice you make, you get a chance to think about it, and to be aware of how another part of the software is going to behave without needing to go and look at its source code. (This argument is essentially “referential transparency is good”.)

Now, it’s entirely plausible that you don’t care about that benefit, and you’d rather have a shorter method that works in the easiest possible way without regard for referential transparency, because your system is small, or #age is hardly called anywhere, or everything else in the application is time-dependent anyway so you don’t need to be reminded of it. That’s fine too! But DHH doesn’t go into any detail on the tradeoff; he just makes fun of the version that takes an argument, because it’s testable for the sake of it, and why would anyone bother with that?

He doesn’t seem interested in exploring the situation, or in interrogating what testability implies in this case, only in laying out his prejudices as if they’re indisputable common sense. They’re not. And I haven’t seen any sensible person claim that TDD is a wholesale substitute for thinking about the design of your software, so it’s misleading to argue against that idea as if it’s representative.

TDD can lead to better designs by providing a simple, learnable, repeatable discipline that makes it more likely you’ll notice design problems sooner. (There are other design benefits too, but this is the least contentious one.)

Some programmers may be so proficient at spotting design problems early that they don’t get any benefit from “testability”, and others may exert enough control over their software’s user-facing behaviour that they are able to dodge complexity at the requirements level, but for the rest of us it’s a useful litmus test for avoiding messy, knotted code. Being testable doesn’t make an architecture good, but good architectures tend to be easier to test.