Just so we're on the same page, test driven development (TDD) is the practice of writing tests before implementing new code. The tests initially fail, but as you build out the new feature/function they should start passing. I've been testing code for years, but until the last year or so I never wrote any tests first.

Full disclaimer: I don't follow TDD practices for all the code I write. Far from it. However, I've found situations where it offers great value, and I think it's worth considering. These are totally my own revelations. I've heard similar anecdotes before and never took it very seriously, but I've come full circle by actually doing it myself.

There are two main areas where I apply test driven development: helper functions / utilities, and state management code. Really any code that is highly reusable and solves a specific problem. For many other programming tasks I still find it gets in the way (maybe I'm doing it wrong), but it can really shine in these areas. The following is a list of benefits I've found can be gained from TDD.

Correct issues before implementation

TDD offers a chance to write real code against your proposed interfaces before you implement them. I know I often start working and encounter scenarios I never thought of. Here that cycle can happen before any real implementation is written. Pre-planning is important, but there's nothing quite like actually writing code. Catching these issues at the test level feels easier to correct.

Tests become enforced documentation

Requirements can get complicated, and can take some time to internalize. When I start coding implementation details, I often get part way into solving a problem and then find I need to reference documentation or notes or something to refresh myself on specific decisions that were made. The context juggling sucks. When writing tests, my code is simpler and maps much more closely to the requirements. Every requirement has a test case. I also can name each test to provide context. Referencing documentation and writing tests almost feels like the same task. In fact, when you're done, the tests serve as a form of documentation themselves, one that isn't just words on a page but instead can be used to mechanically assert program correctness.

Tests are easier to read, share, and get feedback on

It's much easier to show someone my test code and ask if I've met all requirements, than it is to show them implementation code and ask if all requirements are met. "Real" code can take time to untangle; it has its own cognitive load. Test cases are pretty straight forward, and again closely map to the requirements.

With tests written, coding becomes easier, less frustrating

When it finally comes time to write the implementation code, a lot of my work is already done. I've deeply internalized the requirements. I've been forced to design nice API(s) for those requirements. I can't forget about any small details while I'm caught up in implementation problem solving, because tests will fail and let me know. I can divorce myself completely from project notes at this point, I'm really "just coding". This freedom can make coding more fun and avoids frustration.

Continuous, instant feedback

I get continuous, instant feedback on all scenarios as I code. There's no stopping to test how things work so far, I just know. Watching more and more tests pass is just as rewarding if not more so than hitting save and firing up the app to test something manually. TDD also avoids writing tests after the fact against code I've already confirmed to be working manually, which can feel like a chore and potentially leads to sloppy tests.

Tweaking things as you code is painless

Of course tests are great when refactoring and maintaining code long after it was written. But I've come to terms with how often I change little things in the process of coding a new feature, too. I may have only written a function an hour ago, but now I see a better way to do it. Usually I'm pretty comfortable with these small, "inline" refactors, but I often take great care when rearranging things after I've already confirmed they work manually. It can be tedious and a little nerve wracking sometimes. However when I have tests written before hand, these situations are a breeze. If I break something I already wrote, I know instantly, and I know exactly what use case has broken.

Confidence boost

Without having tests written first, when I think I'm done coding a feature it's actually just the start of a testing and debugging process. Sometimes I have to go back to the drawing board after I thought I was done, which doesn't inspire confidence in the program. When I have tests written first in conjunction with established requirements, as soon as all test cases pass I'm done. And I'm very confident that the code is nearly perfect. After I started experiencing this, it's been hard to look back.