I have to, because the world is imperfect. But greenfield is exactly where I find TDD shines. It’s untested code that I have to maintain that make TDD difficult, because untested code is often untestable and inscrutable.
Often even in those situations the very first thing I do, before I make a change, is test existing behavior as I understand it. Just so I have some sort of safety net changing a tiny thing which might have much larger ramifications I didn’t consider at that particular place in the code. Likewise, one of the first things I do entering a codebase in this condition is I start documenting it, with statically analyzable types. Again, testing first.
The only real exception is when there’s such a huge corpus of untested, hard to understand code that exploring the runtime is the most plausible way to get footing to get back to that approach.
I understand this isn’t how everyone feels. But I sincerely find it more valuable than trying to work in any other direction, and it’s only grown stronger the longer I’ve been working in software.
If you're doing TDD exclusively with unit tests I find it falls flat for pretty much everything except complex, logical code with no state and simple inputs and outputs.
Likewise I tried doing TDD on code which had complex, hard to mock interfaces to the outside world (e.g. interfacing with a horrible oracle system) and it didnt exactly become impossible but it definitely stopped being worthwhile.
This is absolutely true. And it’s not a coincidence I embraced TDD at the same time I was learning FP. And I continue to generally prefer both TDD and a FP style no matter what stack I’m working in.
Note: In some cases it makes sense to first write a failing test case, like when you found a bug you reproduce the bug in a test and then you fix it. But it doesn't make sense to write a green field project like that.