When I look at great works of art or listen to inspired music, I sense intimate portraits of the specific times in which they were created.
– Billy Joel
We’ve discussed the philosophy before – Unit Test writing is not just about checking that some of your code works. It *is* that, to be sure. But it is much more – and it is much more because of the discrete, low level of abstraction that the name “unit testing” implies.
Unit testing is checking a single method, on a single type. We should completely divorce this from any kind of test of functionality – these tests are specifically checking to see that an object behaves according to its contract.
This is ridiculous you say. Why would any sane person forget about functionality and check only that her set of objects behaves according to contract?! Madness!
Well – for starters, my dramatic friend, we are only forgetting about functionality within the context of unit testing. Automated acceptance and automated integration tests to a lesser extent are both focused on functionality, and are both very important.
Unit tests, and their focus on the integrity of the object model and the integrity of the thinking that has gone into constructing it provide two big advantages that tests focused on functionality do not.
#1 – They provide pressure in the direction of good design.
The complexity of a test grows disproportionately faster than the complexity of the production code that it is testing. That is to say – if we have gnarly production code – our test code will have to be significantly worse in order to deal with it. Three levels of branching containing five nested loops is not something that anyone is going to be eager to unit test. And really, what’s more of a straightforward, testable unit than one that is doing a single thing … that is that is adhering to the “S” in SOLID – the Single Responsibility Principle. And being able to cleanly mock any dependencies – obviously an important part of testing a single method of a single object, drives toward Open-Closed, Liskov Substitution and Dependency Inversion.
#2 – They separate the composition of the system from the programming of its components.
If you have a set of well-thought-out objects with a suite of unit tests verifying their contracts, composing your system into the functionality that you intend to deliver becomes a different kind of activity. It’s meta-programming – or programming with a new language made up of the objects that you’ve created. You no longer have to worry if the constructs of this new language have integrity – you just have to use them. This language is cleaner, more domain specific, and simpler than the general programming language that your units are based on, so the complexity that you are having to deal with is far less.
Many a struggling rock act has faithfully played clubs and small venues to earn enough money to make a demo recording (less expensive, moderate fidelity) of their music to pitch to music companies. In hopes of really making it big with their music they’ll record these demos, play them for anyone who’ll listen and within months, will have listened to their demo countless times. Every so often a record company will “sign” one of these bands – and provide the resources to record their music in top facilities with top engineers and producers.
I’ve read that a funny thing happens, sometimes – bands will be so attached to their cheaply made recordings that they find it hard to be happy with the objectively top-notch (though undoubtedly different) recordings that were financed by the record company. This effect is called “demo-itits”.
Software engineers are just as prone to get demo-itis as musicians. We don’t like to admit it but we fall in love with sub-optimal code – I would argue almost as soon as we write it. Suggesting that we change our code can feel like a personal insult at times. …hmm, maybe I’m the only one.
Anyway – writing unit tests after production code has already been written almost invariably results in a need to refactor – that is, to change our precious code. This is a significant, though all-together avoidable pressure against writing the tests at all.
The way to avoid this pressure is to just write your test first. Your production code will then evolve with your test – and there won’t be a jarring need to change your code abruptly – that is – demo-itis won’t have a chance to set in.
So to recap – a unit test isn’t really just about checking “that code works” – it’s about helping us achieve simplicity through good design and appropriate handling of complexity. And while it’s not impossible – we do ourselves a disservice with regards to simplicity by writing after the fact tests.
And, as we know, simplicity is what incredible software is all about!!