TDD Revisited Again
Previously I blogged about TDD with some reservations and then later as I re-introduced myself to the pure practice as I had learned it some many moons ago. For the last month, I have been plowing ahead methodically with this experiment, and I thought I’d revisit some of my thoughts from earlier.
First off, the pros still stand in my mind, but I’d like to add one enormous additional pro, which is that you get code right the first time. I mean, really, really right. Right as in coding for a few days without ever running the application you’re working on, and then firing it up and having it work flawlessly the first time you run it.
And, I’m not talking about plodding along with some tiny development exercise like scoring a bowling game. I mean, I recently wrote an application that would parse a text file, format and filter records, and insert the result in a database. Using a combination of Moq for my internal seams and Moles for external concerns (file and database), I exhaustively tested everything I wanted to do as I went, following the red-green-refactor process to the letter. After some hours here and there of working on this (shaking off my TDD rust being part of the time spent), I finally hit F5 and it worked. My files were parsed, my databases updated, my log file written accurately. This wasn’t a fluke, either, because I repeated the process when I wrote another utility that piggy-backed on this first one to send out emails with the results. I wrote an API for setting up custom mail messages, dependency injection for different mail clients, and wireup code for integrating. Never once did I step through the debugger trying to figure out what network credential I had missed or which message was missing which formatting. I put the finishing touches on it with my tests passing and code coverage at 100%, hit F5 and had a well-formed, perfect message in my inbox before I even looked away from my screen, expecting to be kicked into the debugger. That is a remarkable pro.
I mean, think of that. If you’re a skeptic about TDD or unit testing in general, you’ve probably never experienced anything like that. If you have, that’s amazing. I don’t know anyone that doesn’t develop in this fashion that has this happen. I mean, people who are really sharp get it mostly right, and sometimes just have to tweak some things here and maybe step through a few functions a few times, but without ever running the code? I’m not buying it. And even if they did, I come out way ahead this way. A non-tested code base is going to be harder to modify. I have hundreds of passing tests now that will tell me if subsequent changes break anything.
So, I’d like to take a look back at the cons, and revisit my opinions after a month of doggedly sticking with TDD:
- Larger design concepts do not seem to be emergent the way they might be in a more shoot from the hip approach — figuring out when you need a broader pattern seems to require stepping out of the TDD frame of mind
- There is no specific element of the process that naturally pushes me away from the happy path — you wind up creating tests for exceptional/edge cases the way you would without TDD (I guess that’s not strictly a con, just a lack of pro).
- It becomes easier to keep plodding away at an ultimately unworkable design – it’s already hard to throw work you’ve put in away, and now you’re generating twice as much code.
- Correcting tests that have been altered by changing requirements is magnified since your first code often turns out not to be correct — I find myself refactoring tests as often as code in the early going.
- Has the potential to slow development when you’re new to the process — I suppose one might look at this as a time investment to be paid off when you get more fluid and used to doing it.
Regarding (1) and (3), this seems not to be the issue that I thought it would be. I had grown used to a fast flurry trying out a class concept, and realizing what refactorings needed to be done on the fly, causing it to morph very quickly. I had gotten very proficient at ongoing refactoring without the red-green preceding it, so, in this way, I was able to ‘fake’ the TDD benefit. However, I realized a paradoxical benefit – TDD forced me to slow down at the beginning. But rather than really slowing me down on the whole, it just made me toss out fewer designs. I still refactor with it, but it isn’t necessary as often. The TDD forces me to say “what do I want out of this method/class” rather than to say “what should this do and how should it relate to others”. The difference is subtle, but interesting. The TDD way, I’m looking at each method as an atom and each class as a unit, whereas the other way I’m putting the cart a bit before the horse and looking at larger interactions. I thought this was a benefit, but it resulted in more scratch out work during prototyping than was really necessary. I was concerned that TDD would force me to throw away more code because I’d be tossing tests as well as code, but in reality, I just toss less code.
Regarding (2), this has sorted itself out as well. TDD does tend to lead you down the happy path as you’re using it to incrementally correct and refine a method’s behavior, but there’s nothing that stops you from tossing in along with that a thought of “how do I want this to handle an exception in a method that it calls?” This, I’m finding is a discipline and one that I luckily brought with me from testing after or testing during. It is not incompatible with TDD, though TDD does help me not get carried away focusing on corner cases (which can be ferreted out with tools like Pex or smoke tests later).
Regarding (4), this has also proved not to be too much of an issue, though I have seen a touch of this. But, the issue really comes in when it turns out that I don’t need some class, rather than a swath of tests. TDD helps my classes be better thought out and more decoupled, and so, I’ve found that when I’m cutting things, it tends to be surgical rather than a bloodbath.
Regarding (5), this is true, but I’ve fought through the burn. However, there is another thing that goes on here that I’d point out to people considering a foray into TDD. Imagine the scenario that I described above, and that you’re the non-TDD hare and I’m the TDD tortoise. I’m plodding along, red-green-refactoring my parser when you’ve finished yours. You’re probably running it on an actual file and making changes while I’m still working methodically. By the time I’m finished with my parser and doing the formatting, you’ve got your formatting finished and are working on the database. By the time I get to the database, you’ve already got things working 90%, and are busy stepping through the debugger and cursing the OleDbCommand API.
But, that’s where things start to get interesting. Often times, in my experience, this is the part of development that has you at the office late, pulling your hair out. The application is 90% done at 4:00, and you’re happy because you just need to make a minor tweak. Suddenly, it’s 8:00 and you’re hungry and cranky and cursing because no one is left in the office. That’s a brutal 4 hours specifically because you imagined in to be 20 minutes at 4:00. But, let’s say you finally get it and go home. I’ll finish an hour or so after you and you win the first leg of the race, though I’ll do it without a lot of fanfare and frustration, since I’m never hitting snags like that with my methodical process.
But now, an interesting thing happens. We send our versions out for Beta testing, and the issues start coming back. You have your own knowledge of your code to go on as you fix defects and perform minor refactorings, while I have 100% code coverage. I quickly find issues, fix them, see all green and am ready to deliver. You quickly find issues, fix them, and then run the application a bunch of times in different scenarios trying to ferret out anything that you could possibly have broken. I’m gaining on you fast. And, I’m going to keep gaining on you as the software grows. If Beta lasts more than a short time, I’ll win. If there are multiple versions of the software, I’ll win easily. At some point, I’ll lap you as you spend way more time repairing defects you’re introducing with each change than fixing the one you set out to fix. The time investment isn’t new TDD versus seasoned TDD. It’s a little time up front for a lot of time later.
I had tried TDD in the past, but I think I wasn’t ready or doing it right. Now, I’m convinced that it’s definitely the path for me. Somewhere, I saw a presentation recently that mentioned sort of an arc of unit testing. People tend to start out with test-last, and then move to test during, then test-first and then test-driven, according to this presentation (I wish I remember whose it was so I could link and give credit). I think my previous issue was that I had short circuited the arc and was trying to assimilate too much all at once. If you’re a developer trying to convince others of the merits of unit tests, TDD might be a lot to spring on them up front.
I was thinking about why that might be, and something occurred to me from way back in my undergrad days. The first thing we all tend to do is want to write all of the code at once. Since we’re given small assignments to start, this works. It’s hard to turn bubble sort into a lot of different feedback passes — you write the whole thing and then debug the ‘finished’ product. As classes get harder, you try to keep this dream alive, but at some point you have the “aha” moment that you need to get shorter feedback cycles to prevent assembling the whole thing and getting it all wrong. So, you do this by making your code modular and developing components. That carries you through into the early part of your career, and you have a natural tendency to want to get code up and running as quickly as possible for feedback. Then, TDD comes along and changes the nature of the feedback altogether. You’re no longer getting the feedback from your running application, but from your unit tests of small components. So, if you’re building a house app, you don’t start with the House class and run it. Instead, you start with a “Bedroom” class, or even a “Bed” class. While your counterparts are adding rooms to their running houses, you’re getting your “Bed” class working flawlessly. And, while they’re cobbling furniture into their rooms, you’re assembling rooms from their furniture. When they’re debugging, you’re finally ready for your first “F5″ feedback. But, you haven’t come full circle and turned into that freshman with bubble sort – you’ve gotten real feedback every 30 seconds or so. You’ve gotten so much feedback, that there’s little anticipation with the first real run of the component you’re working on for that week. You’re pretty sure it’s going to work and you’re going to be heading home at 5:00.
So, yeah, I guess I’m a TDD convert, and will eat some crow for my earlier posts.