Automated testing is great, but whether it’s easy to write or not depends on your code design. As a rule of thumb, the more loosely coupled your classes are, the easier they are to test. And one can’t talk about loosely coupled classes and not mention dependency injection.
One simple example should clarify how it works. Imagine a class that encountered an error and wants to log it. Let’s also assume you’re using Monolog to create loggers that handle these events. Without dependency injection, whenever the class encounters an error it must now instantiate a logger and use it. If you have 50 classes that use loggers, you would write the code for instantiating a logger 50 times in 50 places (well, you’d probably write a function somewhere to simply retrieve an instantiated logger and that too would be a sort of dependency injection, in a nutshell).
With dependency injection, you would create a logger at some point in time and then simply pass it along to each class that can log errors (traditionally, in the constructor or through a method like set_logger). That way your classes don’t actually know what a logger is or how it works or how to get one — they simply get one passed along.
Normally you wouldn’t even bind yourself to any specific logger but you would use an interface. Luckily, for loggers, there already exists the PSR-3 standard (which Monolog also implements). So your classes simply expect a PSR-3 LoggerInterface object passed on in the constructor.
When you write automatic tests for your classes, you don’t need to actually load Monolog too! You can just pass on any PSR-3 implementing class, or just use Mockery to create one on the fly.
Dependency injection containers usually provide an array of features that are much more powerful than just that (e.g., auto-wiring or on-the-fly instantiation). For our projects, we use PHP-DI but this is not actually a requirement for using our framework. The framework accepts any PSR-11-compatible container and only assumes the features that PSR-11 foresees.