One thing about a good software design is there usually exist a number of layers of abstraction. The problem is that this can start to become cumbersome to manage. It's important, however, to recognize that there are tools out there to mitigate this and thus your design shouldn't suffer as an attempt to "simplify."
Simplification comes through abstraction and encapsulation. If you find your architectural framework complex, you're probably not encapsulating the details enough. I always design with a junior programmer in mind. That is, could a junior-level programmer pick up my code and be productive in a short period of time. If the answer is yes, then the design is "easy" to work with, though it may be "complex" under the covers.
The first way to simplify any design and the inherent testing thereof, is to use an Inversion of Control (IoC) framework. There are many on the market (Spring, Unity, Castle Windsor, etc.) Such frameworks allow for clear separation of concerns without hard-coding dependencies. The real benefit is realized when unit testing. Without an IoC framework, you'd (hopefully) do dependency injection manually, however this would require lots of plumbing to test a single component and lots of manual work. IoC containers make this easier by "automatically" managing the dependency instantiation. I great tongue-in-cheek overview can be found here.
Given all of that, I sometime still struggle with the perceived complexity of my code. Most if the complexity (with respect to dependencies and separation of concerns) happens in the lower layers, from the service layer down. Fortunately, most of this has a one-to-one mapping to a database.
Let me preface the following by saying that this only applies to websites, as opposed to shippable products.
Now, call me old-fashioned, but I don't like ORMs. Well, more specifically, I don't like ORMs when there's little chance of a change to the underlying datastore provider. I don't feel like I get the level of control that I want, and, given my experience perf-tuning applications for production, I feel that I'm justified. I say this because the ORMs abstract away the database interaction and, following my "junior developer" thought process makes it easy for developers to write horrible data access code. I feel it's of much more value for a junior developer to become familiar with a particular database vendor (SQL Server in the .NET world, for instance) than an ORM that may or may not be used at the next job they get. If you understand how the underlying system works, you can then work at a higher level with getter effectiveness.
So, in order to achieve the same effect of an ORM, I simply use code generation. This is far simpler in .NET than in Java to due to certain language constructs. By generating the code, I don't waste CPU cycles at runtime as various mappings and pseudo-SQL expression are worked out. I simply send the query to the db provider. Sure, this SQL could be vendor-specific, but I find that 90% of the time, I can write ANSI SQL query that SQL Server, Oracle, MySql, DB2, etc could process. You might say, however, that I am limiting the reusability of my code. I'd argue that proper separation of concerns shields me from this. I have never reused data access codes across projects since the data model always differs.
Now, the game changes if I'm building a product. In this case an ORM is, by far, the best way to support multiple DB platforms. It's worth noting that even with an ORM, there's still a lot of plumbing and data containers that the ORM maps which should be generated. Anyone that is hand-writing files that correlate to a database table need are stealing from their customers.
In the end, this comes down to using the right tool for the job. I feel that code generation of potentially native SQL is the right tool for a single-db-platform website and, in contrast, ORMs are the right tool for shippable products that can live on any platform. In both cases, though, code generation should play a role in the development process.