We have audited a lot of test suites. The unhealthy ones look the same: a giant pile of end-to-end tests that take forty minutes, fail randomly, and that nobody trusts. Developers stop reading the failures and just hit retry. A green build that nobody believes is worse than no tests, because it gives false confidence.
The shape that works
Many small unit tests at the base, fewer integration tests in the middle, a handful of end-to-end tests at the top. The ratios matter less than the principle: push each test to the lowest level that can prove what you need. If a piece of logic can be checked with a plain JUnit test and no Spring context, it should be.
- Unit tests: pure logic, no Spring context, milliseconds each, thousands of them
- Integration tests: real database via Testcontainers, real Spring wiring, run on every push
- End-to-end tests: a thin layer covering critical user journeys only
- If a test needs the whole system to verify one branch, it is at the wrong level
Integration tests against the real thing
For years teams mocked the database in repository tests, then shipped bugs that only a real database would catch - a missing index, a constraint, a wrong dialect. Testcontainers changed this for us. We spin up the actual Postgres image inside the test run, so the integration tests exercise real SQL against the real engine.
@Testcontainers
class OrderRepositoryTest {
@Container
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
// Spring Boot points the datasource at the container
}A test suite you do not trust is not an asset, it is a forty-minute tax you pay on every commit.
End-to-end tests are a scalpel, not a blanket
We keep end-to-end tests deliberately few. They cover the journeys where a failure costs real money: signup, checkout, payment capture. Everything else is verified lower down. When an end-to-end test does fail, it means something genuinely broke across the system, and people pay attention. That trust is the whole point.
One honest tradeoff: this discipline is harder to maintain than to start. The pressure to "just add one more end-to-end test" for every bug is constant. We push back by asking, in review, whether the bug could have been caught by a unit test instead. Usually it could have.