It's harder to have these layered effects in other languages (in my experience) though often distinctions between a concrete and abstract (interface driven) API are a good first layer.
Unfortunately, most languages today have at best limited support for this idea. For example, plenty of languages with OOP tools offer both some sort of public/private distinction for access control and a concept of implementing one or more explicit interfaces, but these are usually treated as orthogonal features.
As long as you’re working in that kind of framework, or with more dynamic tools where you don’t necessarily have any concept of explicit interfaces at all, it’s going to be difficult to write unit tests without circumventing the normal access control and modularity mechanisms somehow.
Obviously there is not "necessity" here. You can indeed add more coupling with wrong tests. But this is a side effect of some excessive cargo cult about TDD and you should not throw the tool away because some overuse it.
The claim that writing tests helps you define and describe you'r code interface still hold, and strongly.
I usually code in Python and run pylint on my prose. When it tells me I shouldn't call a private method or attribute from outside, it is for me a warning sign:
- I may not be testing the proper behavior.
- If I feel I need to test it inside, this code nugget has too much responsibilities.
- It may be the case that this private thing is really public (I am a private-by-default coder), but this is rare.
When you don't have the time to refactor it properly, I admit that in some cases there are only bad choices: writing a long unatomic scenario testing, testing atomically inners, or not testing anything at all. The later is the easy path for short-sighted development, but adding some testing, even subpar, is much better, because at least it shows Someone Once New How To Roughly Run this part of the code base.
That said, writing good unit tests, doing TDD correctly, etc. are all skills that require experience. Just using TDD is no more likely to guarantee good code than any other software development process. But no process at all is almost always worse.
I feel a bit funny about this, though; it does make the tests explicitly dependent on stuff that, to the rest of the code, is a hidden implementation detail. So, it's probably better to test only the public interface wherever that's feasible...
I hardly test private methods for this very reason. The methods are private and are meant to be helpers. You can poke at it through the method that implements it but I tend to stay away from testing private methods.
To head off the 'only test your public method camp':
I often have bits of math or 'tricky stuff' in a private method, the public usages of that method may limit the input to the private method in some manor or map it in a way that offsets edge cases, i.e. an edge case for the public method may not exorcise the edge case in the private method. I'd much rather just test the full input range and edge cases of the private method directly than leave it up to the next person who comes along. The next person may need to tweak the application logic in the public method and then also figure out how to map the application logic inputs to ensure the edge cases of the private method are tested, seems less than efficient and also prone to being missed. It seems silly and fragile to expect that person to figure out the edge cases for the public method, all the edge cases for private methods and the map of public method input to edge case in private method(s).
As well as the simple fact that your code will be more reliable, it forces you to write your code in a way that is testable, which in turn will make it loosely coupled and modular.
One of the major drawbacks of many current unit testing tools is that in some contexts they can have exactly the opposite effect. If you expose implementation details through your modules’ interfaces that would otherwise be hidden, just in order to support your unit tests, then that makes your code less modular and potentially more tightly coupled.
It could be a direct exposure, for example by adding public methods to a class that allow tests to access otherwise private fields in languages like Java or C++. It could also be indirect exposure, for example using a dependency injection scheme to initialize everything so you can substitute test dummies for live code, but then using something at the top of the tree that essentially has to know everything about everything in your application in order to set up all the injected parts.
These things happen to a greater or lesser extent depending on many factors: which tools you have, how dynamic your language is, and more. However, I would challenge any claim that using something like TDD necessarily implies that your code will evolve a robust, modular design. Extensive unit testing can promote modularity in some ways, but it’s definitely a two-way street in most contexts today.