I've just been writing some unit tests prior to doing a bit of development, and it's revealed some interesting things about the codebase I'm working on.
I think developers shy away from test-driven development (TDD) due to the change in thinking required, which is a shame as that effort upfront saves a lot of effort later on. The key to good TDD is making sure you're writing unit tests as opposed to integration tests - any functionality that relies on reading files, databases, HTTP state or indeed anything external to the component being tested needs to be mocked such that you're providing predictable inputs.
That usually means going some distance along the road to inversion of control (IoC) practices. By which your components say, "I need this, this and this to function" and it's the responsibility of the caller(s) to pass it to them, rather than your component being one large class which reads files, connects to the the network, inspects the session and so on.
So what did I find about the application I was writing tests for?
- Session variables are written and read anywhere and everywhere in cavalier fashion.
- The HTTP session propagates really, really deep into the application code - a long distance away from any of the parts dealing purely with web concerns.
- For an MVC application, the page controllers contain huge amounts of logic that ought to be in models, often leading to inconsistent results and bugs where one controller method is missing logic that another has.
These are all bad things, by the way. Don't do them!
The good thing is that because I'm writing proper unit tests (not integration tests that assume the session is fully instantiated, or "call this controller and hope" tests), I need to fix this. And it turns out to be very easy to refactor a lot of that mess, because the structure I want to end up with is already defined by the need to arrange the mess into distinct units of functionality that I can test by injecting mocks. Classes end up with a single responsibility simply because my testing philosophy doesn't allow them to read files and session objects willy-nilly.
In addition, because my mock objects implement only the barest functionality necessary to run each test, I've caught about a dozen bugs where the application makes invalid assumptions about the state of an object. Even if you don't believe in the value of improving your code structure, surely you can appreciate not having to diagnose "object reference not set to instance of an object" errors.