What TDD Is and Is Not
In my travels, I’ve heard a lot of people embrace test driven development (TDD) and I’ve heard a lot of people say that it isn’t for them. For the most part, it’s been my experience that people who say it isn’t for them have either never really, actually done it or haven’t done it enough to be good at it. Lest you think I’m just being judgmental, here’s a post of mine from about 3 years ago, before I had truly taken the humbling plunge of forcing myself to follow the red-green-refactor discipline to the letter. My language here is typical of someone who buys into unit testing and even to TDD, but who hasn’t become facile enough with the process to avoid feeling the need to tinker with it and qualify the work. If you cut to the heart of what’s going on in my post there (and I say this hat in hand) it’s less “here’s my own special way of doing TDD that I think is actually superior” and more “I haven’t yet gotten good enough at TDD to use it everywhere and that’s rather frustrating.”
Not long after I wrote that post, I forced myself, on a project, to follow the discipline, to the letter, and I’ve never looked back after the initial pain of floundering during what I used to describe as the “prototyping stage” of my coding. I solved the ‘problem’ of TDD not being compatible with that prototyping by thinking my designs through more carefully up-front, solving only problems that actually exist, and essentially not needing it anymore. TDD wasn’t the problem for me when I wrote that post; the problem was that I wasn’t being efficient in my approach.
It’s been my experience that most of the criticism of TDD comes from people like me, years ago. These are people who either have never actually tried TDD or who have tried it for an amount of time too short to become proficient with it. I can rarely recall someone who became proficient with the practice one day saying, “you know what, enough of this — I don’t see the benefit.” For me this statement is hard to imagine because TDD shortens the feedback loop between implementing something and know if it works, and developers innately crave tight feedback loops.
And since the majority of criticism seems to come from those least familiar with the discipline, there are a lot of misconceptions and straw man arguments, as one might expect. So today, I’d like to offer some clarity by way of defining what TDD is and what it it isn’t.
What TDD Isn’t
I think it’s important that I first discuss what TDD is not. There are a lot of misconceptions that surround the test driven development approach to writing software, and there’s a pretty good chance that you’ve at least been exposed to them, if not slightly misled by them. Not only does this make it hard to understand how the practice works, but it may even lead you or your team to reject it for reasons that aren’t actually valid.
- First, TDD is not a replacement for user acceptance testing. Someone who practices it does not believe that it’s a valid substitute for running the application and making sure that it does what the requirements state that it needs to do.
- Classic TDD is also not comprehensive automated testing. The by-product of it is mainly unit tests, which are tests for finely grained pieces of code, such as classes and methods. Other kinds of automated tests, such as integration tests and end to end system tests involving databases or other external constructs will be a separate prong of your overall testing strategy.
- One thing that I frequently hear as an indictment of TDD is that it doesn’t encourage you to think through your design because you initially do the simplest thing to make tests pass. This is not accurate at all. It simply distributes the planning over the course of your development as you go rather than forcing it all to be done up front.
- Test driven development is not and does not claim to be any sort of load testing, concurrency testing, or anything else that you might put under the category of “smoke” or “stress” testing. The tests generated by TDD are not meant to test the behavior of your system under adverse conditions.
- Another common misconception is that TDD means that you write all tests for the system before you start writing your code. Frankly, doing so would be wildly impractical, and detractors who cite this impracticality as a reason not to do TDD are arguing against a straw man.
- TDD is not a quality assurance strategy. When your software department is contemplating new initiatives and dividing them up according to who will own then, TDD does not belong to the testing group.
- Detractors of TDD often point out that it doesn’t address corner cases in application or even class and method logic. That is true, but TDD doesn’t aim to address these. They belong with the other automated tests that will be written later.
- And, believe it or not, TDD is not primarily a testing activity. This is probably the hardest for people to wrap their heads around when learning the practice. But if you think about the acronym – test driven development, it is primarily development. The tests driving it are simply a characteristic of the development.
What TDD Is
Having gone over a series of things that TDD is not, hopefully I’ve cleared up some misconceptions and narrowed the field a bit. So let’s take a look at what TDD actually is. I should note here that the flavor of TDD that I’m addressing is “Classic,” triangulation-oriented TDD rather than the behavior driven “London” school of TDD.
- As mentioned in the last section, TDD is a development approach. It’s a development approach that happens to produce unit tests as you go, which you can then save for later. I suppose you could discard them and retain some of the benefit of the approach, but that would certainly be a waste since you’re going to want automated tests for your system anyway.
- Another facet of the test driven development approach is that you avoid “paralysis by analysis,” a situation in which you are so overwhelmed by the complexity of the problem that you simply stare at the screen or otherwise procrastinate, unsure how to proceed. TDD ensures that you’re constantly solving manageable problems.
- TDD produces test cases that cover and address every line of code that you write. With this 100% test coverage, you can change your code fearlessly in response to altered requirements, architectural needs, or other unforeseen circumstances. You’ll never have to look at the system nervously, wondering if you’re breaking things. Your tests will tell you.
- Besides allowing you to change code easily, test driven development also guides you toward a flexible design. The reason for this is that TDD forces you to assemble your code with testing seams in it, which are entry points, such as constructor and method arguments, that allow you to take advantage of polymorphism for easier testing. A side effect of this is that your code that’s testable is also easier to configure and mix and match in production.
- TDD is also a way to ensure that you’re writing as little code as necessary. Since every change to production code requires a failing test, you have to think through exactly what the system needs before you ever touch it. This prevents speculative coding and throwing in things that you assume you’ll need later, such as property setters you never actually use.
So how is all of this accomplished? Well, TDD is a discipline that follows a specific process and relatively simple process.
- As I mentioned briefly, the first step is that you expose some kind of deficiency in the system that you’d like to address. This could be a bug, a missing feature, a new requirement – anything that your codebase does not currently do that you want it to. With the deficiency picked out, you write a test that fails because of the deficiency.
- With the failing test in place, you then implement the simplest possible solution in your code to get the failing test to pass, ensuring that all of your other tests also still pass. This makes the system completely functional.
- Once the system is completely functional, you look at your quick and dirty fix and see if it needs to be refactored toward better design. If so, you perform the refactoring, ensuring that the tests still pass.
So there you have it: a brief overview of what TDD really is. If you’re interested in more on this subject and you have a Pluralsight subscription, check out my course on continuous testing TDD using a tool called NCrunch, which is all about speeding up your feedback loop during development. Most of this post is from the transcript of that course. If you don’t have a Pluralsight subscription and are interested in a trial, drop me a line and I’ll give you a free week of Pluralsight subscription.