I have tried many times to do TDD. I find it extraordinarily hard to let tests drive the design, because I already see the design in my head before I start coding. All the details might not be filled in, and there are surely things I overlook from the high-up view, but for the most part I already envision the solution.
It's difficult to ignore the solution that is staring my brain in the face and pretend to let it happen organically. I know that I will end up with a worse design too, because I'm a novice at TDD and it doesn't come naturally to me. (I'd argue that I'm a novice at everything and always will be, but I'm even more green when it comes to TDD)
I have no problem writing unit tests, I love mocking dependencies, and I love designing small units of code with little or no internal state. But I cannot figure out how to let go of all that and try to get there via tests instead.
I don't think that I'm a master craftsman, nor do I think my designs are perfect. I get excited at the idea of learning that the way I do everything is garbage and there's a better way. If I ever learn that I'm a master at software development, I'll probably get depressed. But I don't think my inability to get to a better design via TDD is dunning-kruger, either.
You're already doing several reasonable things that tend to improve results: using unit tests, being aware of dependencies, being aware of where your state is held. There is ample credible evidence to suggest that both using automated testing processes and controlling the complexity of your code are good things.
There is little if any robust evidence that adopting TDD would necessarily improve your performance from the respectable position you're already in. So do the truly agile thing, and follow a process that works for you on your projects. You can and should always be looking for ways to improve that process as you gain experience. But never feel compelled to adopt a practice just because some textbook or blog post or high-profile consultant advocated it, if you've tried it and your own experience is that it is counterproductive for you at that time.
My big tip for someone at your stage: don't see the design. See a few designs. Sure, be going in some direction, but constantly be seeking alternatives to choose from. And always favor the simpler alternative to start.
One thing that helps keep me doing that: it's only with trivial problems that you know everything important up front. Accept that your domain will surprise you. That your technology will surprise you. That your own code will surprise you if you pay close attention to what's working well and what could be better.
Maybe you're over thinking it? It sounds like you're already doing the right things.
All the details might not be filled in, and there are surely things I overlook from the high-up view, but for the most part I already envision the solution.
The design part of TDD is just the expectations. So if you were to test an add function for example, you might write something like
before actually implementing the function. So here the design is that the add function takes 2 arguments. That's it.
For other things like classes, your expectations will also drive the design of the class -- what fields and methods are exposed, what the fields might default to, what kinds of things the methods return, etc. Your expectations are the things you saw in your head before you start coding. So it's pretty much the same as what you do already. The benefit of TDD is in knowing that you have a correct implementation and you can move on once things are green.
One thing that's easy to misinterpret is that TDD doesn't mean writing a bunch of tests before writing any code...That's pretty much waterfall development. TDD tends to work best with a real tight test-code loop at the function level.
Incidentally for functions like that, if you have an environment that supports a tool like QuickCheck[1], it's a great thing to use. "The programmer provides a specification of the program, in the form of properties which functions should satisfy, and QuickCheck then tests that the properties hold in a large number of randomly generated cases."
Why is that TDD examples always test stuff that is pretty much useless? I don't need to check an add function. I am pretty confident it will work as is.
If you can find me a more useful example on somewhere then please show it to me.
It's difficult to ignore the solution that is staring my brain in the face and pretend to let it happen organically. I know that I will end up with a worse design too, because I'm a novice at TDD and it doesn't come naturally to me. (I'd argue that I'm a novice at everything and always will be, but I'm even more green when it comes to TDD)
I have no problem writing unit tests, I love mocking dependencies, and I love designing small units of code with little or no internal state. But I cannot figure out how to let go of all that and try to get there via tests instead.
I don't think that I'm a master craftsman, nor do I think my designs are perfect. I get excited at the idea of learning that the way I do everything is garbage and there's a better way. If I ever learn that I'm a master at software development, I'll probably get depressed. But I don't think my inability to get to a better design via TDD is dunning-kruger, either.
I want to see the light.