Understanding the technical practices of software development is not easy for everyone and a lot of otherwise smart people are failing when they try to apply them. Some people even claim that the practices I cover in my book, Beyond Legacy Code: Nine Practices to Extend the Life and Value of Your Software, don’t work.
For example, David Heinemeier Hanson, the author of Ruby on Rails, says that Test Driven Development (TDD) is dead. He refers to what he calls “test-driven damage” where developers write too many tests, and write implementation dependent tests, and go about changing their production code to accommodate their tests.
Justin Searl wrote a blogpost about how TDD doesn’t really work very well. He says it may work well initially but as soon as we need to start to refactor our code, adding classes and methods, it requires us to write new tests and it breaks old tests.
Justin Searl and David Heinemeier Hanson are really smart people and bring a great deal of wisdom to our industry, but they’re missing some key points. I have seen “test-driven damage” before, where developers make compromises in code due to their tests, but this is not a fault of test driven development. It’s the fault of the way we’re using it.
Kent Beck said we should “write tests until bored,” because by the time a developer gets bored they probably have pretty decent test coverage for their code. But TDD is not about testing—at least not exclusively—and we shouldn’t put our tester’s hat on when doing TDD. It’s a different mindset. It’s like putting your editor’s hat on as you’re writing, and any writer will tell you that trying to edit yourself while you’re writing is a recipe for writer’s block. When writing it’s far better to just let the words flow then go back later to do the editing. A similar thing is true when we’re doing software development. The goal of writing code is to create behavior. Focusing on every little thing that could go wrong can block us, and we don’t want to do that — at least not initially.
No one ever said that if you have test coverage for a behavior and then you refactor the implementation to use more classes and methods that you have to add more tests. No one said you have to but the term “unit test” implies that you do. If you see a unit as a piece of code but that was not the intention of the phrase “unit test.” The word “unit” is referring to a unit of behavior. If the behavior that you’re testing does not change, your test doesn’t need to change either.
The purpose of doing TDD is to support us in refactoring our code but if we make up the rule that every unit of code has to have a test associated with it, then refactoring becomes nearly impossible. Suddenly, tests become a major burden instead of a support system. I sometimes refer to our unit test as a safety net that’s there to catch us if we make a mistake. It’s important that a safety net be attached to the ground so it’s stable but that doesn’t mean we want to entomb our safety net in concrete. Indeed, it would be very stable but the whole purpose of having a safety net would be defeated. The same thing is true with TDD.
Many developers buy into the notion of code quality and so they pay particular attention to the software they write to make sure it’s maintainable and easy to work with. But often they fail to see their tests as code as well. While their code is nice and clean, their tests can get awfully messy. If we’re going to do TDD correctly, we can’t treat our tests as second class citizens. Tests are code every bit as valuable as production code and should be treated with the same respect and with the same focus on quality.
If we reject TDD in particular, like some people are claiming we should, then we will be setting the software industry back at least twenty years. Nothing that we build in the world gives us so little feedback as the software development processes that most developers employ. We are dealing with something that we cannot touch or taste or smell and yet we are perfectly comfortable writing it out of our hands and then throwing it into production without any real verification. Because of the complex nature of software, where code is coupled and intertwined together, changing one line of code can affect an entire system’s behavior so the whole system often times needs to be retested even after minor changes are made.
Physical systems do not behave this way, I can knock out a support beam at my house and while the cieling may sag a little, the whole structure won’t come toppling down. Even bookkeepers use something called “double book entry accounting” where every credit has a corresponding debit to make the accounts balance. If the bookkeeper made an error entering a credit then that error will be picked up when they enter the corresponding debit. But there is no double book accounting for software development. Code comes from our heads, through our hands, and right into the computer.
This is not a very reliable approach, so having a way to triangulate or verify code as we write it is incredibly important, and that’s exactly what TDD does for us. In the future, if we aren’t using TDD as an industry then we’ll need something else that gives us the same benefit. We simply cannot progress as an industry without it.