I hate having to make posts like this. Ah well, here goes...
Following Jack’s recent post on TDD on the Reiver Games blog, I posted a “me too” kind of response here. All this has done is increase people’s perception of me as anti-agile; a badge I wouldn’t ordinarily give myself. Admittedly, it’s all my own fault—in an attempt to provide an alternative viewpoint, I’m painting myself as way too black and white. If you’ll bear with me, I’d like to take the time to explain myself. If not, please skip this post.
[continues inside...]
So... my view of TDD’s benefits and pitfalls:
- Benefit #1
- Promotes simple (from a client’s p.o.v.) and relatively decoupled code. Often, you end up seeing methods in Intellisense that are so well named and so well encapsulated that use of the class requires no documentation whatsoever. Many of my best class interfaces have been born in a TDD environment. I love them almost like children.
- Benefit #2
- Automated regression testing: a huge boon. Remember this: the greatest benefit of your unit tests comes from their repeated running, not from the first green light. This is where you really feel the power. Just remember, too, that you may not have been able to cover all bases with the unit tests.
- Benefit #3
- Supports development in small increments, helping maintain a stable product. Whether you check-in a test at a time or a test fixture at a time, the scope of what you’re doing is helping you stay fairly well focused on a small part of the overall system. Working this way allows you to concentrate all your efforts on that subsystem and so you usually end up checking-in highly robust code. Of course, robustness doesn’t come for free, but staying focused will help you.
- Pitfall #1
- Waiting to complete one test before starting the next (something I’ve seen and heard espoused by agilists) makes it too easy to forget you’ve missed important test cases. It’s too easy to fall into a sense of being done. This is what Jack wrote about today. In the past, I tried writing further tests down on paper as they came into my head, but by that lacked any forcing mechanism to make sure I addressed them. Adding test titles as comments in my test fixtures suffered the same problem.
- The only approach that worked for me was to stub (and fail) each new test case as soon as it popped into my head. Or even to spend 5 minutes before starting a new feature, writing failing stubs for all of the test cases I thought I would need to cover. Unless I had a red light to clear, it was too easy to forget to write the test code. And unless I stubbed the test cases up front, it was too easy to forget I’d ever thought of them. It was frowned upon by some, but if it works for you, JFDI.
- Pitfall #2
- Despite evangelism claiming otherwise, many unit tests (esp. those in UI code) do not represent time well spent. When I was still developing in a test-driven style at work, I really wanted to be the good agilist, unit-testing even the UI. In hindsight, I held my project up by doing this. I contributed to its eventual failure. I still harbour a deep sense of regret and guilt for this. And, for that matter, anger and bitterness towards those who insisted I should be writing those tests. I really should have listened to my gut. I had a mind of my own, but I’d been browbeaten into ignoring it.
- Pitfall #3
- Mocking objects is rarely satisfactory. While NMock and its cousins are technically brilliant little libraries, they intrinsically increase the coupling between tests and the production code. This makes large-scale refactorings an absolute f***ing nightmare. I really can’t stress that strongly enough. It’s enough to make you question whether such libraries are really worth their place on this planet.
- When working in UI-controlling code (e.g. the presenter in an MVP pattern), in particular, it’s enough to drive you insane. User interfaces, by their nature, almost always involve an iterative design process. Tests that check that values are fed into the UI at the right time are particularly susceptible to changes, especially if you need to switch from a push to a pull model (or vice versa).
- Of course, no-one’s forcing you to use mocking. It just feels that you’re damned if you do and damned if you don’t. Mocking can easily end up testing the implementation of otherwise encapsulated algorithms (a bad thing, obviously). Not mocking, however, can end up ruining the usability of your classes by exposing too much state in the interface, purely for the sake of the tests. It’s a shit, isn’t it?
So, how much TDD am I doing at work these days? In a word, none. I’m not doing TDD at all and I’m very rarely writing unit tests. And, in the circumstances, I feel entirely comfortable with it.
My lack of TDD is entirely because my work is 99% UI work and experience tells me not to bother. Were I working on the underlying business layer, you can bet your sweet ass I’d be unit testing. Perhaps not doing TDD, as it’s rare that brand new code is written in that layer (in my project, that is), but I’d certainly be looking to cover existing code with a unit test before meddling with it.
I hope this article (for it’s more than just a regular post!) has set the record straight. I’m not anti TDD. I’m not anti-agile. I’m receptive to anything that works. And, after a couple of years of guilt, I feel completely confident in the validity of my approach. Whether you think I’m right or not is up to you. Just promise me you’ll only do what works for you.