Bad code snippet example FTL I guess.
Ok so I posted before about TDD helping you by providing a safety net against incomplete testing and developer bias. The very first comment I got was on my code example (which I did say, to be fair, wasn't actually real code). Specifically:
that initial code is smelly in the EXTREME. TDD isn't the problem, nor the solution... proper coding in the FIRST PLACE is missing here.
Ok, fair point. The code is not great. My first instinct was to immediately jump to the defense of the snippet as being an example only, but, after giving it a second thought, I decided to turn that into my next blog post instead!
The fact is, the real live code was smelly. One problem evident in the snippet I used is that basically the same work is being done in the catch as the try, and in fact once we got in there to look at it, it was pretty simple to refactor out that whole bit of try/catch altogether and put the function together a little more cleanly.
Ok but back to the point. I would like to submit a complementary but also contrary opinion to the comment above. More to the point, TDD is a solution that helps you ensure proper coding in the first place, and thereafter.
The Fallacy of "Proper Coding in the First Place"
So we've all been new programmers right? We didn't come out of the womb writing perfectly factored code. We all had to learn. And to learn to write good code, you have to write shitty code. And watch it break. And learn to fix it. And recognize those mistakes before they happen at some point and design in the catches for them. Eventually you graduate to smelly code. Then good code. Then more smelly code sometimes because you've been up for too long. Then some really sweet code that nobody but you can understand. Then, more smelly code. Point is, nobody gets it right on the fly every time.
Sometimes you have incomplete data as to the requirements or the eventual scope of a project or even individual set of functionality. If you believe in various "best-practice" axioms you will probably just solve the problem at hand, you probably won't be super concerned with future-proofing during this iteration. You don't want to suffer Analysis Paralysis, spending all day prematurely abstracting and architecting, because as far as you know right now, You Aren't Gonna Need It. So you write the code that needs written today.
For whatever the reason, the idea that all code can be properly done up front and completely future-proof is a fallacy. Code that is in need of refactoring is the majority of code out there in the world. Bet me it's not.
The Safety Net Is Still Down There
Enter TDD. As I said in the previous post you could have covered the possible scenarios with a handful of tests and had confidence that you covered all the bases. Now, let's say that you realize, or someone tells you, that this code is smelly. Or say you wrote it when you were new, and you have learned, and you want to fix it. Or say you have to fix it because someone else wrote it and it's too smelly and you just can't bear to live with it. One way or the other, you're ready to sit down and refactor it.
So changing code that is in production, or is a dependency to some other code, or that maybe you wrote a while ago and just don't fully remember all the nuances of, can be very scary. And dangerous. However, if you have the unit tests in place that cover the existing code, you are in a much better position, and can approach your refactoring with confidence.
If your tests fully test the various result scenarios of a given function you can change that function and know that if you change something fundamental to the result of the function, your tests may then fail, and you will have instant feedback regarding your refactoring. You can augment your code and your tests to the extent required, and when it's green, you're good. (Simplified explanation, this isn't meant as a full on article on integration and regression testing)
Contrast that to having no tests in place. There may be code all throughout your application that depends on this code. You might make some change that introduces a subtle but real break on one of many dependent pieces. Thinking back to the bias of your own manual testing, and the fallability of human QA people, you may end up creating a pretty severe break in your application that doesn't get caught until sometime down the line when it's in production and not trivial to fix.
TDD Turns Out to Be the Solution After All
In my estimation, at least, TDD solves many problems that occur throughout the lifecycle. If you are test-driving your initial code, you have a better chance of getting it (functionally) correct the first time. Later when you realize that, for whatever reason, either the function or design of the code needs to change, having tests in place can ensure that you don't introduce breaking changes to dependent code. It's a process that repeats over and over throughout the life of any non-trivial application - change happens. It's better to have the safety net when making the changes, whether new functionality or refactoring smelly code, than to not.
No Silver Bullet
Now like I've said before, TDD is not magic. You aren't going to magically never break code or miss a bug or introduce a breaking change again just because you wrote some unit tests. You need to practice writing them to cover more and more scenarios. You need to combine them with other tools that should be in your toolbox as well for you to achieve synergy. Design principles like DRY and SRP, patterns like Dependency Injection, coupled with good testing are practices that will further enhance the robustness and elasticity of your code. And as you start to seriously get into the Test-First mindset as opposed to the Test-Because-I-Have-To one, you will see better design start to be derived in your code as you try to keep your tests from being smelly. Get out there and test people.