Test-Driven Development (TDD) has been around a while; you may have heard of it. If you’re new to the idea, I’m going to try to sell you on it. If you’re familiar with it, I’m going to make the argument that it’s the best way to approach new projects. And if you know it and hate it, I’m going to try and change your mind, if you give me the chance.
To put it simply, TDD is a technique for software development where you write your tests first, before you implement your features (code). The idea is that it will save you time in the long term by avoiding mistakes.
TDD bases itself on the philosophy “Red, Green, Refactor”. Red is when you write the test, but you don’t have any code. When the test is done, you begin implementing the code until the test passes (Green), and finally, you Refactor to clean up and adhere to your principles. (If you are new to TDD, I encourage you to read Test Driven Development By Example by Kent Beck.)
I want to sell you on TDD, so let’s start with some promises that I want to make to you. With practice and discipline, I feel TDD provides the following benefits:
TDD validates your system throughout the development life cycle. You gain a great deal of confidence in your code, because when you’re done with a big feature or refactor, you run all your tests and you find problems up front. When it’s time to commit your changes, you don’t have to worry that you broke something. It offloads all of that work that you normally do after a project is “done” and helps you to do it right the first time. Less bugs. More fun!
Proper TDD lends itself well to certain development principles, and thusly it can help you write better code. It can even help steer you when writing an overwhelmingly complex application. It’s as much a guide as it is a safety rail!
Writing your tests is like writing proofs for your homework. You write a test for a new feature, implement it, and now that proof exists for the life of your application, or until that feature is pulled. When you implement another feature further down the line, and it breaks an earlier test, a good test will take you right to the line number with a detailed description of the problem. The test code that you wrote is almost a form of documentation and provides context for the situation. It reminds you of what “this thing does” and “how it’s supposed to do it”. Behavior Driven-Design (BDD) with Gherkin syntax will take that a step further and create a human readable spec that drives your tests. Near the middle and end of a lifecycle, development is such a breeze. This brings me to one more benefit; I had to save it for last:
That last one is hard to swallow for some… how could it be faster? How could it take less time? To understand this, I think it requires a change of perspective.
In all my career, I’ve heard a lot of complaints about TDD, but I feel they are misconceptions. Let’s discuss some, and I’ll explain why I think they are wrong.
It sure feels like it, at first. Every new TDD project is like climbing a big snow hill, the start of the climb is the worst.
When starting a new project, with or without TDD, some goals remain the same:
Without TDD, we want to get to MVP fast. We write the classes first, and then a test (sometimes). This makes sense, this feels natural and quick. Yet because of this habit, sometimes we skip the test. This leaves gaps in the application’s stability. In the long run, we find ourselves fixing more bugs after the project is “done”. Refactoring puts the entire application at risk. QA becomes more difficult. Mistakes get out. Customers aren’t happy. Stress can rise, and by then, the honeymoon period of a new project is long gone. Our original goals always feel so pie-in-the-sky by this point, don’t they?
With TDD, that can all change. You can achieve the goals we mentioned earlier and avoid the nasty things. Instead of ramping up fast, you front load the work and do it right the first time. Mistakes happen, and things change with time. You’ll still need to refactor, but what if you could refactor without compromising your entire system? What if you could rest assured that new features didn’t break old ones? This is what I mean by doing it right.
Imagine that the way we commonly approach development is a wave.
Projects often turn out like this because of the volatility of the code meeting the applications requirements while new features are being developed. Without testing, new features or bug fixes could create new bugs, breaking requirements that have been previously met. We speed up, slow down and speed back up. A constant flux is felt between developing the application and consistently maintaining all these requirements. Heaven forbid a solution-wide refactor is ever needed, or you’re on thin ice.
Consider instead a graph like this:
This is how a project typically looks with TDD. The start of the project is going to take a while to get moving. You have more to do at first; you need to create your project but also a test harness, and you need to start writing tests before you have code. As you gather requirements and begin implementing them, you are making future-you very grateful. Future-you is going to feel confidant and care-free because after you complete a few sprints for your project, you will have a well-structured code base that is flexible and stable. Every requirement you’ve worked to meet along the way is validated by a test. Any changes you make in the future will likely not compromise the system because your tests will catch problems for you. This results in fewer bugs, easier maintenance, easier design, and ultimately less time.
It’s More Work
It sure feels like it, at first. But consider the point made previously: in the non-TDD approach, it feels like less work at first until you start encountering some bugs. Once you implement a feature and it breaks another, you spend time investigating, solving, and pushing another release. You might not realize the time spent, because it’s so normal. We constantly battle this flux and never bat an eye at the expense. It’s expected. But when you think about it, that is more work.
It sure feels like it, at first. You’re going to make mistakes. You will write bad tests. I remember the struggle, thinking “How do I write a test for this feature when I don’t even have any code to test yet?”. There’s really no arguing this point; TDD is such a different approach to development. It gets much better with practice. You will learn from your mistakes, and eventually you will feel just as comfortable jumping into a new test as you would jumping into a new class. It comes with time, but once you get there it’s a lot like riding a bike!
Many think of testing as “extraneous” to a project. It’s like a loose appendage that hangs off. Like an appendix: we needed it once but now it’s just there. Don’t think of it like this! Think of the tests as part of your code. They aren’t extra, they validate your system and ensure stability throughout its life. It’s integral, critical, priority. Love your tests, they’ll save your butt someday.