Test-driven development (TDD) has been around for more than 20 years. Nearly all of the greats in software underline it as an important and valuable method. Sadly, a lot of developers still don’t do it in practice.
Many people think of TDD as a methodology for testing because of its name. There are some negative feelings towards writing tests. You might see testing as something boring you have to do to prove that the code you wrote works. You write a test, see it turn green on the first run, and are pleased when you’ve got the code coverage above 80%. Maybe you like developing by running or debugging the code every once in a while. Once you see that your code works, testing just feels a bit pointless. Or maybe you have a tester in your team, so the tests you write feel even more redundant.
This all builds towards some negative feelings when writing tests. The result seems obvious. If you dislike testing, why would you bother with TDD?
It is not about testing, it is about design
For me the reason is simple. TDD is not about testing at all. TDD is about design and early feedback. When practicing TDD we write code (in a test) to drive the implementation.
To do that, we immediately have to start thinking about the design. What dependencies does my code have? Will I need to fetch any data through an API or repository? Should it? What about the language? How should I name this class? What name should I give to this method?
TDD allows us to focus on these questions one at a time, by taking small steps towards a working solution. I’ll come back to this in the following section.
By thinking about the design before any actual implementation has been written, we’re already making choices about the most important part of our code, the model.We have the opportunity to focus first on the bigger picture, instead of getting laser-focused on the details straight away. This perspective is often not considered in a test-after approach.
And, because we’re using the brand new model ourselves in the test, we get feedback before anything has even been implemented. Our test acts like a blank canvas on which we start painting the crude outlines of our model. This outside view on your code at such an early stage is really helpful in determining if you are on the right path. Because there is no working code yet, there’s also little coupling at that stage. That makes trying different routes very easy—if you dare to do so.
Incremental design and fast feedback
Another aspect that helps is taking small steps by only specifying a single aspect of the code at a time. That means we write a single test before moving on to the implementation. By taking small steps we ensure that each step is as easy and short as possible. What do you imagine is easier, lifting 500 kg in one go or doing 50 repetitions of 10 kg?
Every time we complete ared-green-refactor cycle we feel rewarded. Because we take small steps, we feel rewarded very often. Also, because refactoring is part of the cycle, we do it often and thus keep the refactors small and simple each time. This constant refactoring of code and test improves the quality of our code immensely.
Getting feedback early and often really enables you to go faster. Your mistakes are smaller and have less impact. Compare it to frame rates in gaming. Getting more frames per second is a huge advantage!
Living specification
After that, the resulting test is like a code-first specification of what the implementation needs to do. This living code-artifact of our incremental design is insanely helpful for anyone touching your code after you. They now have a playground in which to explore the code. If they are unclear how a unit behaves in certain cases it’s easy for them to append the specification with a new case. They can also build on top of what you did more easily. They can rewrite or add to the specification in such a way that it fits the new use case, and then change the implementation.
If they find a bug, they can quickly validate how the unit behaves when presented with the erroneous case. After that, it’s a small step to specify how the unit should behave and implement the fix with confidence. Because your test always has to remain executable for it to remain green, you can be certain that it is always up to date. The test will fail if the implementation breaks the specification. Text-based documentation does not have this advantage and will get outdated the moment it is written.
The real value
As you have read, TDD is not about testing in the classical sense. TDD is a habit where you design code using code, and it will help you:
- think about your models first.
- use short cycles to give you feedback more often.
- enables you to get a different perspective on your code.
- see your tests as ”living specifications” rather than after-the-fact tests.
Plus, it’s also a lot of fun, thanks to the short cycles of failing, succeeding and refactoring. It really makes you look at your tests in an entirely different way. It takes some time and real effort to adopt TDD properly, but it is one of the most valuable development habits you can pick up.