This week I’m at Codemash in Sandusky, Ohio. Today was the second day of pre-compilers and I spent the day doing a Test-Driven-Development (TDD) Workshop. It was a great experience, thanks to George Walters and Guy Royse for organizing it!
I went into it excited because I havent’t written unit tests in quite a few years now and I wanted the refresher. Still, I wasn’t sure what to expect. This is the first time coming to Code Mash and I’ve had it beat in to my head over the last year or so that TDD is dead or useless.
We paired up and were given our specs, called a ‘Kata’ which is basically a practice problem. It was a spec sheet for the domain layer of a game largely similar to Dungeons and Dragons which I’ve never played.
I ended up in a team of three with two other C# developers who had never done any TDD work, but fortunately had a lot of experience in playing D&D so they were helpful in explaining the rules to me. We chose NUnit as a Test Framework because it was easiest to get the ball rolling.
What is TDD?
Before we started our project for the day we went quickly over how TDD works, and how it works with pair-programming. In essence you want to go into a red-green-refactor loop, where you write a unit test that doesn’t pass, then write the code to make that test pass, then refactor that code. There is a lot of nuance and arguments about how to do it ‘right’ but really this is kind of like religion and sports - there are no right or wrong answers.
Writing unit tests
A unit test is a small hunk of code that checks the functionality of some other code. Usually tests are in their own test project, so its easy to keep it from getting deployed to a production scenario. You write them in an “Arrange, Act, Assert” kind of pattern where you set up the test objects, call the functionality under test, then make sure the results are what you would expect.
Pairing while doing TDD
‘Pairing’ in a group of three was rocky at first. Both because we had never met each other and my two partners hadn’t done any TDD before. We got into a rhythm of one person would write a test, the next implement, the next refactor, while all collaborating on the code. The contrived nature of the Kata made it difficult to focus on the dead-simple beginning requirements, like “A Character should have a name”, which is understandable when you’re a seasoned D&D player, knowing the nuances between different versions.
Still, the rhythm eventually hit and we started banging out tests and implementations.
A surprising peril: we ended up with MUD
One thing that took me by surprise was that while our code was well-tested and working, by a few iterations we had a giant ball of MUD (pun intended)! Our
Combat module handling the attack by one character to another was a single method taking two monolithic
Character objects and performaing the surprisingly complicated interations. But hey, all our unit tests passed so that was okay, right?
This took me by surprise because I previously saw TDD as a way of guaranteeing that your code would be clean and well-designed. But I found that in a way, it was easier to keep the Big Ball Of Mud because we could make awkward changes in the complex
Attack method or our monolithic
Character class because the tight feedback loop from our unit tests made it easy to find the bugs. We could just keep hacking into our monolithic methods with little repercussion.
So the solution, naturally, is to apply more effort into your refactoring. TDD won’t make you instantly a good architect.
The feedback loop
The feedback loop you get from TDD can really get you into a ‘flow’ state quickly. I noticed that a failing test makes it easy to drill straight down into the cause of the error. You don’t have to worry about re-creating the state of the application from a CSR or a QA person, you have all the pertinent information right there in your IDE. (hows that for acronyms!)
I think the most satisfying part of the experience was the first time a unit test we had previously written broke unexpectedly. We caught a regression! Had we not had that unit test, the bug would have snuck away on us, and in a real-world situation that sort of thing can be very complicated. Dealing with regression errors in real-world applications can be very frustrating, not to mention expensive. That failed unit test really demonstrated to me the maintainability that TDD can provide for an app that has been marred by regression errors.
Application in the real world
After this experience, I look forward to applying TDD to real world applications, both legacy and greenfield. I think its going to be an uphill battle to learn how to use them in real programming but I think I’ll be a better programmer and I’ll write better programs for it.
Whew… now back to Code Mash!