I recently read (yet another) post proposing that TDD “doesn’t work” and that we shouldn’t use it, or should use it very little. What was shocking about this article wasn’t the content, but the fact that it is hosted on a Microsoft site.
While I’m not entirely on board with throwing out TDD, the one thing I will agree to is that learning TDD is difficult. I am also willing to admit that, to a large extent, TDD is broken. If you’ve been following my post for a while, this SHOULD be shocking news.
And so, I’ve been thinking.
Maybe we’ve been asking the wrong question. Maybe, instead of asking “How do we encourage people to implement TDD?” We should be asking, “How do we make TDD either unnecessary, or trivial to implement?”
So, let’s review why we think TDD is a good idea. One of the first reasons that you’ll find is that when TDD is done correctly, it encourages loose coupling of code. In fact, one of the things that make TDD hard to learn is that our code tends to be SO tightly coupled that it can’t be tested. There are whole books about how to decouple legacy code so we can get tests around them. There are problems with this, but we’ll get to those in a bit.
The next major benefit of TDD is that it allows us to refactor our code without worrying about breaking anything. This is the feature I like the most. I was working on a system recently where the requirements were being “leaked” to me a bit at a time. I’d get something done and it would be “oh, I forgot to tell you about…”
I was able to add the new code up until the last, “Oh, I forgot…” And that broke my whole pattern. But it forced a much better architecture.
Now in the old days, before I started writing tests, I would have been really nervous about tearing the code apart and putting it back together again. What if I broke something that already worked? But, because I had a large suite of tests around this code, I could refactor, run the tests, and fix the tests until I was back to everything working. Without the tests, I would not have known that I broke anything.
BTW, no one has found any bugs in this code yet and it has been almost a year since I wrote it.
Related to refactoring, by having a suite of tests in place, we can be reasonably sure that, as we add new features, we aren’t breaking any existing features.
Next, TDD forces, or it should force, incremental coding.
I was surprised when I found out that people don’t program incrementally. I always write just enough code to move the project along and then test it. But I know people who write code for hours and even days before they run any of it. And then, they can’t figure out where they broke the code. TDD done correctly, doesn’t let this happen.
And finally, once you’ve learned how to test, TDD actually increases the speed with which you can produce quality code. I know a guy who brags about how fast he can code. But I also know he’s only counting the time until he said he was done, not the time it took him to fix all the bugs he found. Is he really any faster than any other programmer? It is hard to judge. But studies have shown that using TDD leads to greater over all speed and less technical debt. Something to consider.
But for all those benefits, there are still some major problems with TDD as a practice.
The first and most looming of all, is the fact that using TDD introduces A LOT of friction into the programming process. As an exercise, I wrote a demo application using TDD while learning a new framework. I knew the language and the testing framework, I was just learning a new programming framework. The problem was I not only had to learn the new framework, but the framework has its own helper functions for testing that I also had to learn.
I would say this friction is what holds most people back and is often voiced as, “TDD slows down my programming.” Which is true until you’ve learned it all.
The tendency with TDD is to try to get 100% code coverage. This often lulls us into the belief that we’ve tested everything that can be tested. But the problem is, because our code often has hidden parameters, we can’t always be sure we’ve written enough tests to be sure that we’ve covered all the possible ways those parameters show up. So, the fact is, we can’t be sure we have 100% coverage of the situations our code may run into even if we have 100% coverage of our code.
The next two disadvantages impact relatively new programmers. That is, people who have been programming for less than 5 years. The problem is, that’s half of the programmers available to the market. After 5 years, you’ve either been promoted to management where you eventually forget how to program or, at the very best only know what you knew when you were promoted, or you’ve moved on to a new career. And then there are people like me who can’t stop coding.
The problem with programmers who have less than 5 years of experience is that they don’t have any concept of design patterns and they know nothing of architecture. And unless you work in an organization that has senior level programmers with more than 10 years of experience, you aren’t likely to know it is even something you need to learn. And here is the problem. It is nearly impossible to implement TDD if you don’t have a broad understanding of these two subjects. If you have less than 5 years of experience, you are still focused on the basic problem of naming things, keeping your code DRY, and making sure you are implementing the single responsibility principle.
And finally, there is simply nothing that is making you write tests. Your code will compile and run just fine without them. The syntax of your language really doesn’t care. So given all the detractors, it is really easy to just not write them at all. There, I’ve said it. Yes, we SHOULD use TDD, but it is impractical to expect any but the most seasoned programmers to use this methodology. And by then, they may not need them as much.
But what if we could write code that didn’t need to be tested? Well, generally any code you can look at and be sure you know what it does and what you can expect is code you don’t have to test. Code with a cyclomatic complexity of 2 or lower, which still allows for a condition or two. Code that is:
- No conditions
- No hidden parameters
- No side effects
By “small” I’m talking about code that is, ideally, 5 lines or fewer with the occasional 10 lines in the rarest of instances.
No hidden parameters. This is a big one. Think about code you’ve recently written. If you are writing Object-Oriented code, you typically have a method that takes a parameter or two (maybe more) and then accesses a member variable and may even create a new object in the middle of the function. The reason we have Dependency Injection is to give us the ability to inject mock objects into our tests so that we can write test against something stable and known rather than these hidden parameters that are constantly changing.
“No side effects” is a specific instance of a hidden parameter. Anyone try to test code that accesses a database? What if all the code you wrote that had some side effect was isolated to a specific area of your program? In that case, it would reduce the number of tests you would need to write to just that area. And if you wrote that code correctly, the tests would be trivial to write.
And this is why I’ve fallen in love with the concepts behind Functional Programming.
In Functional Programming you have a series of functions that you chain together. Much like fluent programming in the Object-Oriented world, the return value of a function becomes the input for the next function. Each function does just one thing, so they implement the Single Responsibility principle probably better than any class you can possibly write. Each function only works on the parameters it has been given, so you don’t need to worry about hidden parameters and the function is not allowed to generate a side effect. Even your conditions are handled by functions that filter the data before passing it on. This all makes your code REALLY easy to test if you decide you need to test it at all.
Of course, no one ever said we couldn’t have side effects. Those are all isolated to specific code that is super easy to test.
If this is all new to you, I suggest you take a look at the ReactiveX and start learning this library in the language of your choice.