It can sometimes feel like TDD is pretty dead in 2022. I’m not sure what happened, because I think it’s a genius way to work, but most programmers I talk to seem to think that it’s a waste of time. We don’t really have to be dogmatic about it though. It’s not equally valuable at all times, and there are even rare times when a total kool-aid drinker like me doesn’t do it. This post is meant to chat about the extremely high-value times.
I work with a lot of people that almost never do TDD but a few seem to pull it out a lot when they’re fixing defects. It makes a lot of sense. When you’ve got a defect, you’ve first got to make sure you know how to reproduce it. If you write that code to reproduce it first, you’re 90% of the way to doing TDD. Add an assertion to validate that your app code does the right thing, and then fix your app code to pass the test. This really makes you feel like you’re making your code bulletproof and you won’t have to hear about this particular defect again.
I’ve had a few examples of complicated problems that would have been almost impossible (for me) to solve without TDD. I once wrote a kafka broker for node.js that ended up being the only Kafka broker in node.js at the time, used by a few big-named companies. The protocol was all binary and I worked from the protocol documentation to write tests first, and then the code to pass the test. I had my test environment hooked up to a real kafka server so that when a test passed, I knew it really worked. I slowly worked my way through the protocol documentation this way until it was done and working. I did a ton of refactoring along the way and a ton of refactoring afterward as well, and the tests gave me confidence in my changes and a very tight feedback loop. Whenever I can tell that something is going to get really complicated, I switch gears to slow, methodical TDD.
There are a lot of people that I’m not going to sell with this point, but it’s been the biggest thing for me with TDD. If you’re working on a codebase that’s going to grow larger and larger over time and be worked on by many people, you need to think about the internal organization of that codebase (“architecture”) if you want to keep it from quickly becoming a big ball of spaghetti. Not only do you need to think about architecture, but you also need to think about the testability of that architecture regardless of whether you write the tests first or after. Driving out architectural pieces using TDD ensures that things are factored and working together in a way that is testable. In this way the last D in TDD is more like “Design” than “Development” because the tests have you architecting the codebase differently. Some will argue that it’s not the beautiful design that they would like, but I’ve often found that the factorings are quite elegant and that testability is more important than beauty anyway. In my experience, when you’ve architected like this, you can bring on the hordes of test-later programmers pretty successfully and get great tests and great coverage. In my experience, a codebase is testable much more easily if it’s TDD-able.
If you’ve got something that’s untestable or hard to test and you’re struggling to get tests on it, it can feel like it would be even harder if you had to write the tests first. In general I find the opposite is paradoxically true, and this is another case where the last D in TDD is more about “Design”. Forget about that working code that you wrote and just try to write a small test like how you wish a test of it could look in the simplest human-readable way. Now see if you can write code that can pass it, ignoring your previous app code. Concentrating on the test first drives out a testable design and I believe you’ll almost always like the factoring as well. Sometimes the test is the hardest part, so starting with it can be the best way to make sure you nail it.
Anyway, these are just my opinions after a decade of TDD and working with many others that seem to only use it occasionally. I still think TDD does a better job of getting tests and well-factored code in 99% of scenarios, but these 4 are the truly magical scenarios for me. My advice is to give TDD a chance in these cases if you’ve given up on TDD entirely or if you never saw the point.