I spent at least a few weeks in the same situation you're in: always reading that IoC was definitely the way to go, but not quite catching the "vision" yet. I kept reading and re-reading articles on the topic until I felt like I sort of had a handle on it, and then I began re-writing some parts of our code to use DI. As I did this, I began noticing pain points that made me realize all the places that I was not correctly separating concerns, making appropriate use of interfaces, and otherwise making my code modular and refactorable.
TDD is also one of those things that is hard to see the advantage of at first. You think, "I already know what I expect this method to do. It does what I expect it to do. Why should I test it?" But when you start writing a unit test, you're forced to think about it in terms of "What input might I actually get in this method" and "What would I do with it?" You start realizing in certain cases you should probably be throwing a particular type of exception right at the beginning of the method, rather than allowing the method to return an empty set (which the consumer should never expect). Or you realize that the way you've written this class makes it impossible to unit test, and there's actually a far better way to set it up that reduces complexity.
Eventually you get a lot better at writing your code, and writing your unit tests is so quick that it's no big pain at all. In fact, your unit tests provide better documentation on the expected behavior of your code than if you wrote big comment blocks. Besides this, I find that even on the "simple" methods that I think I couldn't possibly have written wrong, the unit tests find a bug about 50% of the time.
It has taken a long time and a lot of work, but our code is of a much higher quality, and will be far more maintainable, just because the DI and Unit-testing practices forced us to separate code better.
Edit
I should also mention that we have been able to leverage dependency injection to do things that would have been next to impossible before. For example, I just made it so that when actions are taken by a Quartz job, they show up as having been performed by a specific "system person" for a specific domain. If I were constantly referring to "HttpContext.Current" or even to some utility class that refers to it, I would not have been able to do this. But because we're using DI, any class can simply depend on an ISessionManager
, and the IoC Container's Kernel can decide whether it should be using the HttpContext-based implementation or a special "System" session manager.
There are a number of tricks like this that have made our lives much easier. For example, we don't have to tell each logger instance what class it's in because the DI framework can automatically tell it what class it's getting injected into. I could go on and on. Assuming you do DI the "right way," you'll eventually be surprised that you ever programmed any other way.