Why TDD is a non-negotiable
The reasons why Test-Driven Development should be a necessity and not just an option in the current state of the software industry
The problems
After more than half a decade after its invention, and more than 20 years after its rediscovery by Kent Beck, Test-Driven Development is still a nice-to-have in most companies. It is still far from being mainstream, known to be used only by software geeks.
There are many factors at play such as a lack of care from the developers’ side, poor environments set up by the management, misconceptions about topics, and of course not forgetting the fact that TDD is not easy. TDD is not something that we just take and plug into our development process and it works. TDD is an entire way of producing code with the support of best practices. It is the gold standard of designing software that incorporates all the other best practices such are taking baby steps, doing continuous refactoring, and writing testable code. It is the most efficient way to write software. But also the most difficult one.
Code-as-text cannot provide us with a clear understanding of how the program will behave. We can only know for sure how the program works and what the program does if we execute it. There is an unavoidable lag between the moment when we make a change to the source code and the moment when we learn about the impact of the change we’ve made. This lag is the source of many problems in software engineering.
The problems coming from delayed feedback loops are significant, severe and many times critical. Extensive use of a debugger, lost domain contexts, sneaked-in bugs, risky refactorings, and painful maintainability are all expensive tasks, requiring additional time, effort, and energy. Companies spend an incredible amount of time and money on managing and fixing bugs. They think bugs are inevitable and they are a normal part of the development process. They leave refactoring later in the hope of gaining time, then the refactoring never happens. And even if it happens it will be both risky and expensive. They set up a dedicated quality department for testing their software, resulting in long lead times. They want to add quality after the fact but they don’t have all the domain knowledge anymore. Teams also have trouble with continuous delivery, because their code is not designed well. As a good friend of mine, Mario Cervera once said:
You can’t be agile if your code sucks.
The solution
Test Driven Development provides a solution for all of the problems mentioned above. Debugging times are minimized, domain contexts are captured on time, bugs are prevented, continuous refactoring is practiced and our code will be clean and easy to improve. TDD encourages best practices, forces us to revisit the design, gives smells about our code, and results in a quality test suite acting as living documentation.
Test-Driven Development is not a testing approach. It is a design approach, driven by automated tests. The tests are the inputs of the design, whereas the clean design and code are the outputs of TDD. Starting with a failing test will help us to design the external view of our code. One of the main power of TDD is that we produce APIs with verified user experience (UX). As the tests are the first user of the code, we test the UX of our API before even implementing it. It helps us to avoid expensive mistakes and rework. The biggest part of the design will happen in the Refactor phase of TDD. TDD is a way of growing the design by doing continuous refactoring. Our main goal is to produce clean, domain-centric, and maintainable code. We can only produce clean code and keep it clean by practicing continuous refactoring. TDD promotes aggressive refactoring that ensures clean code.
TDD also helps to achieve both simplicity and correctness, which are the two main traits of every quality software product. Simplicity is the art of maximizing the amount of work not done. The TDD flow forces us to write exactly the code we need. No more, and no less. To write the minimal production code to fulfill the requirement expressed in our test. No unnecessary logic and overengineered code. We strive for minimalist code instead of a gold-plated solution. We go from the Red to the Green phase in the simplest way possible. Otherwise, we would end up with code that we might never need. Remember YAGNI.
Clean code requires simplicity. But without correctness, we can’t have clean code. You can have the most elegant code solution but if it does not do what you expect from it, it is just a valueless piece of code. No code is better than incorrect code. The code written with TDD does exactly what we want. It behaves according to the expectations specified in our tests. A failing test makes sure that our expectation is clear and unambiguous. A passing test means that our code is correct and on point. And refactoring ensures that our design becomes and remains simple. This continuously positive feedback loop makes sure that we are always up to date on the state of our code, resulting in high confidence in its simplicity and correctness.
TDD also ensures testability by design. Because of the test-first principle, we are forced to have a design that is testable. Otherwise, we would not be able to write any failing test for a code that is not testable. To make a behavior testable first we need to define the public APIs and the expectations in a test. We make a contract describing what we are testing and what the expected behaviors are. Then by making the test pass, we fulfill the contract, resulting in both a testable and tested code. There is nothing that guarantees testability when following Test-Last Development. TDD guarantees it by design.
And the test-first approach comes with another killing feature: to produce a quality test suite. If we want to trust our code, we need to be able to trust our tests. If we want to trust our tests we need to be able to see them failing. If we never see our tests fail we don’t know if our tests are actually testing the right things. Seeing a test failing is as important as seeing it passing. A test failure validates that the test is meaningful and unique. It’s a software expectation to fulfill. It gives us a target to aim for, showing that the code we are about to implement is useful and necessary.
And finally, TDD helps to minimize debugging time. Being an expert in debugging is a smell. It means that we waste and spend way too much time debugging our code. Debugging is not bad per se. It is just a huge time-consumer. Every time we need to use the debugger, it implies that we do not know how the code is working. It indicates a lack of frequent testing. Every time we need to spin up the debugger we should ask the question of how we reached that point. And the reason mostly is that we increased our feedback loops. It implies that we lost touch with the state of our code. And the more we are in this phase, the bigger the mistake we can make and the higher the price we will pay for fixing it. TDD fixes this. TDD turns the painful and exhausting hours of debugging into pleasant and joyful minutes of writing tests. When we write code with TDD, we can almost forget to use the debugger. We run all our tests after every baby step, so we always know if and how the code works. Every minute, all the time. Test-Driven Development is a fully automated short-cycle-based debugging.
Conclusion
These are the main reasons why TDD is non-negotiable for me. There are situations where TDD might not return the investment or it is simply impossible to use. That’s a fact. But when it is appropriate, I literally see no reason not to use it. Given the fact of how efficient, adequate and powerful it is to design our code with it, it becomes a no-brainer for me to apply whenever I can. There is no silver bullet in the software industry. But if I would need to name one thing that is the closest to a silver bullet, then I would immediately say Test-Driven Development. TDD won’t provide a fix to all of our problems. But it will prevent most of them. TDD won’t help us to go much faster. It will help to go much better. TDD is not a silver bullet. But a magnificent tool we call Test-Driven Development.
Appreciation and future
This was my first post here, thanks for reading it to this point. In the future, I am planning to cover topics in more detail about Test-Driven Development the other Extreme Programming practices. I believe we can all do better with the ultimate goal of making our customers happy. We gotta move the needle forward and raise the bar in the software engineering space. We have work to do. Let’s go!
Can we consider unit testing to be a part of TDD?
Great article, I myself am retraining with latest tech after a short absence and am keen to implement TDD into my routine