My main gripe with it is the second-order concern that it encourages testing practices that are frankly not very intelligent.
How you should approach testing depends on what kind of function you are testing. Pure functions of their inputs should be tested with property-based tests. If you have
bool isPrime(int n)
the whole rigmarole of "make a test that fails, then write the least amount of code that makes it pass" brings you to something like: assertFalse(isPrime(1));
assertTrue(isPrime(2));
assertTrue(isPrime(3));
assertFalse(isPrime(15));
and so on, where what you really want is to say something like: for all 1 < i < n . n % i != 0
This obviously works less well when you have to deal with the real world, but even in that case TDD leaves you with a patchy and inflexible approach.In those cases dependency injection just increases the SLOC with the payoff that you are able a bunch of trivial unit tests that'll probably never catch a bug.
Integration tests as a default have a the best ROI in those cases.
That always feels like a post hoc justification to me. If you demand 100% test coverage on a Java/C# project then you will almost certainly end up with some kind of dependency injection at the end. Whether that is useful or not is often debatable.
That’s what Spring claims but the truth is that using interfaces enables those things. Dependency injection might encourage the use of interfaces, but it also inserts an abstraction between tests and the things you want to test.
In my experience dependency injection frequently obfuscates system interactions and invalidates any assurances you might get from good unit tests.
Dependency injection is just a tool. It can be useful but is often misused and doesn’t perform magic.
Using a DI container is not required to implement the DI pattern, it's just a tool that facilitates some more complex DI.
I tend to prefer to keep my projects simple enough that manual DI is quite appropriate and readable.
There are good and bad implementations of dependency injection (or, perhaps, appropriate and inappropriate depending on requirements), but the principle of dependency inversion applies at an architectural level, before specifics of implementation and things like unit testing come into play
Another angle is the encapsulation of storage. Using in-memory storage for tests makes Ci pipelines very quick and production storage might evolve over time to accommodate scaling requirements (sharding and such).
I like dependency injection for things which have state and/or do IO and/or are expensive to construct.