Programming Practices: Part 2 – Thoughts on TDD
Well, it seems the last post wasn’t too controversial. Let me try something that might be a bit more controversial. Heck, it might even get some people down right agitated with me but that’s OK, disagreement is a useful tool to drive clarity and understanding.
I don’t like Test Driven Development. I don’t just not like it, I think it’s a bad idea.
How’s that for an inflammatory statement? Probably gonna make the cool kids black ball me 🙂
I don’t spend a bunch of time debating the fine points of the various popular development techniques with people so I suspect someone is going to jump in and tell me I’ve missed the point of TDD – and maybe they are right. Let me refine my statement a bit and then launch into both what I like about TDD and what I don’t like. So here’s a little softer statement.
I don’t like a literal interpretation of TDD.
If I think about what the goals of TDD are (and I’m doing a bit of reverse engineering to get there), then here is what I like about TDD:
1) Focus first on how you are going to use a thing, not the thing – I watch many people early in their software development career (including myself) make the same mistake over and over. They have an application to build. They envision and architecture, decompose the components and then start building them. After a few years they start to realize that the contracts between the components are really important so they start to get very rigorous about encapsulation, abstraction and contracts but their overall approach is the same.
I can’t tell you how many times in my life I’ve done this and spent hours or days building enough components to get reasonably high up in the dependency stack and then go to build the next layer on top (it might be the UI or it might be a service interface or just a higher level abstraction) and realize, crap, it’s all wired wrong. The API isn’t really built the way I like, the components aren’t factored quite right, etc. The result is that i have to write more code at that next layer up than I should have to and the code doesn’t flow very well.
The way I now approach this problem is that I always start at the top level (or a very high level) layer in the system and work down. Specifically before I write any code for a class, API, layer, … I write one or more samples that use the API to to accomplish some purpose. This shows me what I want the flow to be an what the natural abstractions are for the typical use cases of the service that I’m building. After each sample, I go back and visit previous samples and refactor until I get all of the samples using a consistent API/abstraction, resulting in a minimal set of code and clear and easy to understand flow.
For example, when I created the framework I’ve built for managing asynchronous UI in Windows Forms, before I created a line of code, I took the WinForms designer and wrote 6 different dialogs. Each of them demonstrated some important characteristic or extensibility point I wanted to have. The first was to show the bare minimum code one would have to write to get a dialog that would load asynchronously, be cancelable and give the user appropriate feedback. The next showed that I could pass parameters to the background loading process, implement a refresh model, etc. Other samples demonstrated how to coordinate the work of multiple background threads, different ways of displaying progress, different ways of notifying the background thread that it has been canceled, etc. All the dialogs were fully written (and rewritten several times) before I wrote a line of code on the async framework. While I didn’t cover every part of the API, I had a very clear picture of the API/contract/abstractions that I wanted and could begin to conceptualize the design of the async framework. Of course, as I built the framework, I learned even more and went back a few times and refactored my samples but the samples always guided the work.
In a sense, you can think of these samples as test cases – in fact I often use them that way. Sounds a little like TDD, right? It kind of is. It’s also got some characteristics of Behavior Driven Development (BDD) but in a little bit I’ll tell why I see it as pretty different than a literal interpretation of TDD. My first vote of confidence in TDD though, is that I do believe it is a technique that can help you focus on how to use something first and later on what it does.
2) Don’t write code you don’t need (YAGNI) – YAGNI stands for You Ain’t Gonna Need It. Another very common mistake I’ve seen in my career (and, again, made myself many times) is to try to imagine the ultimate end of where a program will go and build an architecture/implementation that sets you up to get there. Good developers are always thinking about all the cool new features/requirements they’d like to add down the road and they’d like to make sure they build their code in a way that enables it.
The problem is that, no matter how hard you try, you can’t predict the future. You can sit here today and imagine what the requirements are going to be 6, 12, 24 months from now. You may believe you see very clearly where it should go but you don’t. Please take my word for that. It’s a very hard conversation to have with an eager developer who really wants to “build it right”. Don’t. Build what you need now and don’t build more. Down the path lies lots of unused code, unnecessary abstractions, complexity that no body understands the need for, etc.
The issue is requirements change. I promise. 6 months from now you will look back and wonder what you were thinking. Because you’ll have people actually using your app and they’ll be giving you tons of feedback on stuff they want and it won’t be the things you thought they’d want. I’ve lived it 100 times at least.
So does that mean that architecture doesn’t matter and you should just put on blinders and build exactly what you need this very moment. No, I’m not quite saying that. Design your code with clear abstractions and generality in mind. Design it in a way that is extensible and composable. Just don’t add requirements that you don’t currently have. If you build a system with a clean, well factored architecture: encapsulation, separation of responsibility, clear contracts, etc, your code will be in a good position to tackle new requirements as they come. But assume no matter what you do, you’re going to be doing some refactoring of that code when you get there. You might as well have less of it to refactor.
Back to TDD. I believe TDD helps you get here. It’s a very rigorous focus on only writing the code that you can concretely identify that you need. I like that.
A note on extensibility – I was discussing this post with a colleague yesterday and he asked me to make sure I made a point about extensibility. Nearly all developers like to make their code extensible. However, many developers believe that their own implementation is “special” and has requirements that can’t be met by the extensibility interfaces. Down that path is a bad extensibility model. If you can’t build your own implementation in the same extensibility framework that you are providing for others to use, I can almost guarantee you they won’t be able to use it either.
OK, so enough about what I like about TDD. Let’s get to the juicy stuff – what don’t I like:
1) I find it inefficient – If you read my last post then you know that I refactor like crazy. The idea of having unit tests that cover virtually every line of code that I’ve written that I have to refactor every time I refactor my code makes me shudder. Doing this way makes me take nearly twice as long as it would otherwise take and I don’t feel like I get sufficient benefits from it.
Don’t get me wrong, I’m a big fan of unit tests. I just prefer to write them after the code has stopped shaking a bit. In fact most of my early testing is “manual”. Either I write a small UI on top of my service that allows me to plug in values and try it or write some quick API tests that I throw away as soon as I have validated them. Once the code has started to settle, then I go back and write the unit tests that I’m going to use to help with regression testing down the road. I don’t mean by this, that I wait until the app is done to build the tests but rather I do it iteratively at a component or requirement boundary.
2) Backing into an architecture – This is probably by biggest issue with a literal TDD interpretation. TDD says you never write a line of code without a failing test to show you need it. I find it leads developers down a dangerous path. Without any help from a methodology, I have met way too many developers in my life that “back into a solution”. By this, I mean they write something, it mostly works and they discover a new requirement so they tack it on, and another and another and when they are done, they’ve got a monstrosity of special cases each designed to handle one specific scenario. There’s way more code than there should be and it’s way too complicated to understand.
I believe in finding general solutions to problems from which all the special cases naturally derive rather than building a solution of special cases. In my mind, to do this, you have to start by conceptualizing and coding the framework of the general algorithm. For me, that’s a relatively monolithic exercise. When I’ve got the basic framework in place (I may still have parts of it still stubbed out), then I start testing to make sure all of the special cases are handled by the algorithm the way I pictured (and of course, as I said in my last post, I do this by stepping through all of my test cases in the debugger to see exactly what it is doing).
I’m not saying that it isn’t possible to build a good architecture following TDD. I’m saying that lots of people are already inclined toward piecemeal architecture and I believe literal TDD enables these natural tendencies too much. I also find the constant moving back and forth between test case and code to be too distracting while I’m trying to hold the design in my head and get the core algorithm fleshed out.
I like many of the goals of TDD but I don’t like some of the mechanism to get there. I suppose we’ll see how many of you are avid TDD proponents and maybe we’ll have vigorous debate about the pros, cons and interpretations of TDD. I look forward to it.
Till next time…