Overcoming Test-Driven Damage
I’ve seen some claims that test-driven development is dead. People refer to “test-driven damage,” where developers write implementation-dependent tests, then change their production code to accommodate their tests. Others say TDD may work well initially, but as soon as we start to refactor our code, adding classes and methods, it requires us to write new tests and breaks old tests.
This is not the fault of test-driven development. It’s 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 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. The goal of writing code is to create behavior. Focusing on every little thing that could go wrong can block our flow.
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. You may think the term “unit test” implies that you do—if you see a unit as a piece of code. But that was not the intention. 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 tests 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: It would be very stable, but the whole purpose of having a safety net would be defeated. The same is true with TDD.
Many developers buy into the notion of code quality, 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 are 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.
Because of the complex nature of software, where code is intertwined, 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.
This is not a very reliable approach, so having a way to triangulate or verify code as we write it is incredibly important. That’s exactly what TDD does for us, and we should embrace that benefit.