But your philosophy is correct and we are in agreement on that.
The thing I was referring to is the abstraction of dependency injection itself. It leads to code that is over complicated as the abstraction itself is not only pointless but adds unnecessary layers of complexity.
There are ways to target the philosophy of what dependency injection for unit testing is trying to accomplish without introducing the side effect of unnecessary complexity that comes with dependency injection. It's as simple as just separating IO functions from core logical functions. Don't bundle these two things together into an object or a single function, that's it. (Also don't use objects period, but that's another topic.)
I find it natural and convenient to think about "What dependencies on other parts of the system does this code have?" Expressing those dependencies explicitly feels like it reduces complexity, not adds it.
But, I'm not talking about pulling in a big fancy DI framework, just making dependencies explicit in your function/class parameters.
I will say that DI is sometimes used as a tool in overly-abstracted systems. An example that comes to mind is the ASP.NET MVC framework -- with DI and inheritance, literally any behavior can be overridden in fairly opaque ways. Trying to suss out the concrete behavior details is like swimming in quicksand. (Or it used to be that way, haven't touched ASP.NET in a while.)
As an aside -- I'm curious about your programming language of choice. I think DI is a lot more useful in some PLs than others. For example, I find JS code often uses imports to create complex graphs of implicit dependencies, and DI can help tame that complexity. But for other PLs like Python or Clojure, I basically don't use DI at all.
Now Java and C# are different because they are compiled. You need a DI tool to do dynamic dispatch if you want to mock.
You can't inject a nuclear reactor into a burrito in any language untyped or not because it will lead to an error.
The only difference is python/js the error will happen at runtime while in C$/java the error happens at compile time.
The main difference is the type. In typed languages you need describe am interface or a class of types if you want to do mocking or dynamic dispatch while in dynamic languages you don't need to explicitly define this as a type, the definition exists in how you use the object that is passed in.
Either way DI, however universal, in any language is bad practice.
Yeah, mocks are just a way to zero out the dependencies so you can focus on the unit you're actually trying to unit test. If all you're doing is zeroing out dependencies, you're not testing much. Every mock you bring in carries and assumption, and those assumptions need to be tested, too.
Cases like that really call for some small scale integration unit tests, where you actually bring in your dependencies and test them all working together.
EDIT: What I'm working on currently doesn't have a hardware component to it, so this might not be practical in the future
I find it valuable at times, depending on the context, but I agree that extensive mock testing like this can get quite useless real quick.
I don't know that I'd word it exactly like that, but one thing I've found over the years is any tests we have that rely on mocks basically just don't test anything at all.
If your whole test is "This gets called, then that, then that", you aren't actually writing a test. You're repeating what the code already says.