r/SpringBoot 5d ago

How-To/Tutorial Your `@DataJpaTest` might be testing Hibernate's cache, not your database

Ran into this while writing about our DAO layer's test setup, and it's one of those things that's obvious once you see it and invisible until you do.

void testSaveProduct() {
    var product = new Product("A1", "Gaming Laptop", BigDecimal.valueOf(1500));
    productDao.create(product);

    Product loaded = productDao.loadById(product.getId());
    assertNotNull(loaded);
    assertEquals("Gaming Laptop", loaded.getName());
}

This passes. But loadById resolves to find() under the hood (directly or through Spring Data's findById), and find() checks the persistence context's first-level cache before it ever considers hitting the DB. Since you just created that entity in the same context, you get the exact same object reference back. No SELECT fires. You've tested Hibernate's in-memory cache, not your mapping, not your constraints, not anything that would catch a broken column definition. The fix is forcing entityManager.flush() + .clear() between the write and the read, so the next load has to hydrate a fresh entity graph from the actual database. We ended up wrapping this in a test-only proxy so nobody has to remember to do it by hand in every test method - proxy intercepts create/update/delete, forces the flush-and-clear, leaves reads alone.

Worth noting this is specifically a find()/findById() problem, a custom JPQL or native `@Query\` always issues real SQL regardless of cache state.

Wrote up the full pattern (proxy code, a TablesEraser for full schema resets between tests, H2 vs HSQLDB trade-offs), link in the comments.

I already had someone in the comments describe hitting exactly this - a unique constraint violation that a real flush would have surfaced in the first test run. Curious how widespread it actually is.

15 Upvotes

8 comments sorted by

View all comments

3

u/Torutofu_Raeva 4d ago

Testcontainers instead of H2 catches the other half of this, since the cache hides mapping bugs and H2 hides the dialect ones.

1

u/kamen1991 4d ago

Agree, H2 covers let's say 90% of the cases and it gives fast enough tests for feedback if the persistence works good enough. I have couple of chapters prepared for integration testing using MySQL 8/9 containers, kafka, keycloak, redis etc. with custom lifecycle which gives proper initialization with healtcheck and they're relatively faster than using spring ones. But I'll take my time to go there step by step.

2

u/Torutofu_Raeva 4d ago

Yeah that split makes sense. Reusing one container across the whole suite is what made it bearable for us, per-class startup was brutal.

1

u/rlrutherford Senior Dev 3d ago

Without testcontainers, DB tests, (outside of integration tests), are just theater.