Doctor: So don’t laugh.
In addition to being a joke, the above is also a succinct summary of Michael Feathers’ book Working Effectively with Legacy Code, recommended to me by Chris Hanson.
“Legacy” in this case means “anything without unit tests,” which means just about all code out there. Including, I’m ashamed to admit, most of the codebase of the Neutrino project I’m currently working on.
Why, after all that effort I put into adding unit test capabilities to my project, have I mostly failed to follow through? Feathers’ book describes it well. Currently, neither programming languages nor external libraries and frameworks make it easy to separate the unique logic of your code from all the dependencies that it requires to actually get its work done.
My codebase, for example, does a lot of custom drawing to a view, and does that drawing in response to calls from the cross-platform engine that actually drives the process. So I’ve got two huge dependencies: the engine, and the Cocoa framework. How do you get around that?
But it’s not just Neutrino. I prefer to work on code that’s in the middle of complicated systems, doing interesting and complicated things.
The classes in Kent Beck’s Test Driven Development: by Example were small data model classes that dealt only with each other. Well, sure, that would be easy! Real world code is rarely so straightforward. Beck describes a world so far removed from mine it might as well be in another galaxy.
Feathers, on the other hand, gets dirty in the trenches. But he doesn’t reveal any magic formulas. Instead, over and over again, he says: stop laughing. Change the code.
Does the class you want to test rely on a well-protected singleton? Change the singleton so it isn’t, even though that allows others to use it incorrectly.
Does the class you want to test have some private methods? Make them public.
Does the class you want to test rely on other classes from a library or framework? Change your code not to refer to such classes directly, even at the cost of added complexity and overhead, so you can break the dependency and use dummy objects instead for your tests.
All these are ugly solutions, design-wise. And Feathers knows it; he makes multiple apologies for his techniques. But he stresses that they work.
Well-designed code, code that only allows itself to be used in the way it needs to be for production scenarios, turns out to be anathema to unit testing.
When I programmed primarily in C++, I looked askance at the typeless nature of Objective-C. How will the compiler check that you’re doing the right thing? What I’ve found since is that very few errors result from this extra freedom from type that you have in Objective-C. Maybe all those safeguards and extra complexity in C++ weren’t necessary after all?
Similarly, I wonder if the next major programming language might be optimized for unit tests. It will have even fewer compile-time restraints, even fewer security restraints, but many mechanisms to make it easier to test what you’ve just written – maybe even require such tests, or auto-generate such tests.
Until then, I’m going to see what the results are when I make my Neutrino code a lot less jolly.
It’s true you know, a LOT of those compiler checks are, most of the time, pretty worthless.
Proof? Ask ANY long time Python user. A lot of us are able to do some pretty fantastic things (like, oh I don’t know, Google?), and we don’t have any private methods. It’s strongly typed, but DYNAMICALLY typed, which gives you so much freedom.
And because of it, unit tests are SO easy to write, in fact, they’re often a joy to write. My last big project had more unit tests than actual project code! And trust me, when I left the company, the guy taking over the project was very grateful for it.
For an example, see the documentation on the new testing module, doctest:
http://docs.python.org/lib/module-doctest.html
And I’m sure you’ll see the same kind of fun stuff in other dynamic languages, like Ruby.
There I go, evangalizing Python again…
Did somebody say Ruby? Check out… this… page… that I can’t find any more. Dammit.
“Fear and loathing” has long been my watch-phrase when it comes to strong typing. I hate it, and always have. I think it’s useful to have your compiler (if you have one) do some rudimentary type-checking, but enforcing a strong typing philosophy is simply the hallmark of a bondage-and-discipline language.
I think there are certain elements of high-level language programming which are so integral to the process – loops, branches, types – that if people need to rely on strict constructs in order to be able to program anything, then they’ve lost before they’ve begun. If you don’t understand typing to be able to get it mostly right, most of the time (and fix the other cases during debugging), then you probably are in the wrong line of work.
For some reason when I finished reading this, I immediately thought back to an OO language that never seems to have caught on — Eiffel.
http://en.wikipedia.org/wiki/Eiffel_programming_language
It is a “design by contract” language which seems like some early thinking of TDD… each method/routine can have pre and post conditions and asserts and so on.