Lately there has been a lot of talk (again) about TDD.
This twitter thread is worth exploring, with all its branches.
Also, I found worthy to read this post by Hillel Wayne. I do disagree with a few things he says, but forces me to think (and now I want to look a bit more into TLA+, which I heard about before but never investigated).
One thing to point out before we start is that the majority of the work I have done revolves around Line of Business and e-Commerce. So there are things for which I can’t assert that the below or my experience holds water. At the same time, the “we are special so it doesn’t work for us” excuse I have found so far to not be reality.
In the beginning
I learned TDD with the idea that forcing myself to write tests first would force me to create code that was easier to change. Having to setup how to use code, will force me to design it in ways that would be easy to use. I will be thinking about who has responsibility of a piece of logic. TDD has been always a design approach for me.
Is not easy
TDD is a practice. As such, requires skill, and you need to develop the skill, just like with any other. It is not just: I am going to do the test first. It is thinking about what needs to be accomplish and how you divide responsibilities to accomplish that. It is thinking about how you create those tests, which modify how you create the methods, even the class. Or from a post that I have linked before because is so well put: TDD in 3 easy steps
You can still create bad code out of a TDD approach. I think is more difficult to do it, but it is possible (looking back to my earlier code here).
One of the things that TDD has helped me a lot is to find holes in requirements. When the information provided is not enough to cover all your bases. Or when that information is wrong. As soon as you start coding you find the limits of the knowledge you have.
There are other tools that can be used (formal methods, for example). But combining TDD with pair/ensemble programming allows me to discuss about options and not only raise the concern, but provide possible solutions.
Another advantage of TDD is the quick feedback loop between thinking about some code and seeing it actually affect functionality. Instead of going the wrong path for a long time, as I have seen it happening outside of TDD, you can quickly home into a working solution that does what you need (which is different from the perfect solution, see next)
An issue that always shows when people talk against TDD is that they always forget about the refactoring step. Maybe it helps that I also have on my mind all the time that sentence from Kent Beck: Make it work, make it right, make it fast.
Without refactoring, I will not find the perfect solution, or even a good solution. I will find a solution that works. And now that I have a solution and a set of test that I can use to confirm I can change the code without changing the expected behaviour, I am free to try improvements on the code.
Refactoring is important. You do it constantly. TDD makes the process easier because I will know quickly if I have messed up.
Micro vs Macro
Macro design is selecting where your house is going to be, what kind of materials you will use for your structure, how many rooms is going to have, which type of rooms. The decisions that are more difficult to change.
Micro design is going into each room and deciding what is going to be on that room. What kind of furniture, which kind of flooring, how easy is going to move the furniture that you have in the room. The decisions that are easier to change.
TDD is good for micro design: Design around classes and methods, modules and functions, and their interactions. But as soon as you try macro design it fails because, even when you do an outside-in approach (my favoured one), there are decisions that just can’t come from it. TDD will not help you discover that you need to use an event bus.
There are other tools at your disposal that help you on creating architectures and the connection between systems.
Happy Paths vs Sad Paths
With TDD, my main objective is to setup the Happy Paths of my code. What it will do under normal circumstances. Once that is done, I maybe add some handling of errors if there is anything special about them: if I want to throw exception (exceptions should be exceptional), or if I want to use a Result type.
Sad paths (failures) are more numerous than happy paths (or at least that seems to be the case to me). Apart from the basic ones to decide the design of how to deal with bad situations the rest could be added later.
Tests are a valuable output of TDD, but not the main focus: design and refactoring confidence are.
Andy Hunt put this question in twitter: Could we increase adoption of TDD by calling it something else?.
I don’t mind the name Test Driven Development (or Test Driven Design, development and design are one and the same). I always saw tests as one of my responsibilities. Especially once we started automating tests. Testers bring other skills to the plate than just writing of tests.
Tests represent for me what the system do. A test is behaviour. Though if giving it a different name makes it easier to “sell” to those who have a different view about their responsibilities, sure, name it differently.
My belief is that a change is required by CEOs and Heads of departments. You always get advertisements asking for this or that tech. That should not be the focus of your hiring process. Tech is the easy part to teach. Convince the people at the top in changing their parameters of search, and you will get TDD or other disciplines that increase quality more spread. That involves improving the hiring process as well.
When TDD is not enough
I have talked about micro vs macro. But there are other weak points of TDD.
Certain algorithmic solutions are difficult or impossible to achieve with TDD. Whatever you do, the solution will not come out of the standard TDD flow. Sometimes, sitting in front of pen and paper could be more useful. My first decent solution of the 8th Queens was done that way. Or the quicksort implementation shown in Hillel’s post.
Sometimes you need to do exploratory coding, to find how a library works, or how things can interact together. Something like the Lisp REPLS are fantastic for this kind of exploration. Some of the design that happens on TDD can be done through the REPL. Though the benefits are better for dynamic systems (at least in may experience)
Sometimes, you don’t really know what you are trying to accomplish. TDD will not help you find your objective.
Deep testing I don’t think can be achieved by TDD. Property testing, mutation testing, exploratory testing, … are things that are not really on the remit of TDD.
Sometimes certain qualities of the software remove some (but not all) of the need of TDD. For example, Hindley-Milner languages allow you to do some design just by easily creating types, and is a joy combining Sum and Product types to define the basics of your system.
Formal Methods could become quite helpful on a few contexts (my experience is very limited with them).
It is useful
TDD is a tool. Is not the only tool on your belt. I have found it that has helped me immensely in creating the right working code, with high maintainability, and easy to test. I tend to regret when I don’t do it, as invariably I have to return to that code to start fixing design decisions taken.
But if there is something better, even if is just within a context, I will use that. I am never going to let my current preference to stop me from finding something that helps me create better systems.
Finally, TDD is not a substitute to the desire of creating good systems. No practice or tool is.