I think that what changed my mind, other than the dubious joy of maintaining my own code a couple of years down the line, was the realisation that while I don't know the solution to the problem yet I do know what each function I'm writing is supposed to do and thats what I should be testing. As a side effect it also meant that the functions I was writing became smaller (easier to test) and it became easier to see what the end solution might look like because I could understand the intermediary steps more easily.
Or to put it another way, I might not know how to write the compiler, say, but I do know that I'll need to start off by reading a line of input and I can test that I'm doing that properly, etc.
My test suite had simple programs in C, and used emcc to compile and run them, then compare the results from a known-good implementation to the one I was writing.
As I went, I would add one new instruction to the program at a time.
That's a perfect example of outside-in testing, and evolving the requirements, tests, and code simultaneously.
External compiler tests may seem TDD-like because they exist first, and make the compiler fail until the required features are working.
TDD is about bogging down the code with internal tests that treat every aspect of the code as a committed feature.
You cannot do a single thing that doesn't first fail some newly added test, which then goes green.
ATDD works though.
I definitely use "it's hard to test this code" as a smell indicating refactoring is in order.
I've never tried TDD, but I suspect most of the value is that it forces people into practicing separation of concerns and other basic anti-ball-of-mud practices.
My early sins included happily relying on field injection in Spring (don't), happily blasting stuff into Ruby objects (don't), mocking my software's world into an unrecognisable comic book universe of gravity-defying nonsense (don't) and so on.
A common criticism of TDD is that poor tests make things harder to change, not easier, and I believe it, because I have absolutely done that.
So a test that has no production code it is testing.
Preferably in a language with really fast turnaround, a "scripting" language.
After a while, I have a self-contained test that does something, and does it reliably.
I can then refactor the test by extracting the production code.
This isn’t my usual TDD workflow but I use it when my understanding of the mangauge or language feature are blocking me.
So skip TDD if you know exactly what you're doing or have little clue. Use it if you want dopamine hits to motivate continuous progress on average complexity tasks. If it comes out wrong, commit a checkpoint locally, but attempt a refractor now rather than repeatedly paying the debt.