Careeroza
Testing Optimistic Locking
Two Transactions in One Test
@DataJpaTest wraps the whole test in one transaction, so you cannot simulate two users. Turn that off and drive transactions yourself with TransactionTemplate:
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(PostgresTestConfig.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED) // no test-wide transaction
class ProductLockingTest {
@Autowired ProductRepository products;
@Autowired PlatformTransactionManager txManager;
@Test
void secondStaleUpdateFails() {
TransactionTemplate tx = new TransactionTemplate(txManager);
Long id = tx.execute(s -> products.save(new Product("Mixer Grinder", new BigDecimal("3499"), 10)).getId());
Product asha = tx.execute(s -> products.findById(id).orElseThrow()); // version 0, detached
Product ravi = tx.execute(s -> products.findById(id).orElseThrow()); // version 0, detached
asha.setStock(9);
tx.executeWithoutResult(s -> products.save(asha)); // version -> 1
ravi.setStock(8);
assertThatThrownBy(() -> tx.executeWithoutResult(s -> products.save(ravi)))
.isInstanceOf(ObjectOptimisticLockingFailureException.class);
assertThat(tx.execute(s -> products.findById(id).orElseThrow().getStock())).isEqualTo(9);
products.deleteAll(); // no automatic rollback now
}
}Note: Without the automatic rollback, clean up your data (delete it, or truncate tables in an @AfterEach) so tests stay independent.
What to Test in the Persistence Layer
| Test | Why |
|---|---|
| Custom JPQL / native queries | syntax and results on the real database |
Migrations + ddl-auto=validate | scripts and entities agree |
| Query counts on hot paths | N+1 does not come back |
| Locking / version conflicts | concurrency behaviour is as designed |
| Soft delete and tenant filters | no hidden or foreign rows leak |
Plain derived methods (findById) | usually not needed: Spring Data already tests these |