package com.ankurm.hibernatedemo.testdb; import static org.assertj.core.api.Assertions.assertThat; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Backs docs/09-testing-in-memory-databases.md, chapter "Schema isolation: create-drop vs rollback vs * DB_CLOSE_DELAY". Uses one shared {@link EntityManagerFactory} across two ordered tests to * PROVE the pollution, then a second, correctly-isolated pair to prove the fix -- both against * plain H2 (the pollution mechanism is not H2-specific, but H2 is what the rest of this repo * already uses for its default datasource). */ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class SchemaIsolationTest { private static final Logger DEMO = LoggerFactory.getLogger("DEMO"); private static SessionFactory pollutedSf; @BeforeAll static void openSharedFactory() { // DB_CLOSE_DELAY=-1 is what keeps this in-memory H2 database alive across the many // short-lived connections each Session opens and closes -- without it, H2 tears the // whole in-memory database down the moment the LAST open connection closes, and the // next openSession() call would silently reconnect to a brand-new, empty database // instead of failing loudly. pollutedSf = TestDbSupport.buildSessionFactory( TestDbSupport.Db.H2, "isolation-demo", "create-drop", TestDbWidget.class); } @AfterAll static void closeSharedFactory() { pollutedSf.close(); } @Test @Order(1) void testA_insertsARowWithNoCleanup() { Session session = pollutedSf.openSession(); session.beginTransaction(); session.persist(new TestDbWidget("AAAAA", 1, true, "left behind by testA")); session.getTransaction().commit(); session.close(); DEMO.info("testA committed a row and did nothing to clean it up"); } @Test @Order(2) void testB_seesTestAsRow_becauseThereWasNoIsolationBetweenThem() { Session session = pollutedSf.openSession(); long count = session.createQuery("select count(w) from TestDbWidget w", Long.class).getSingleResult(); session.close(); DEMO.info("testB sees {} row(s) -- at least one is testA's leftover, proving pollution when isolation is skipped", count); // The point: testB did not insert anything itself yet, but the row count is already // >= 1, because testA's commit was never rolled back or dropped. assertThat(count).isGreaterThanOrEqualTo(1); } // ---- Now the same shape, but done correctly with @Transactional-style manual rollback ---- @Test @Order(3) void testC_insertsARow_thenRollsBackInsteadOfCommitting() { Session session = pollutedSf.openSession(); session.beginTransaction(); session.persist(new TestDbWidget("BBBBB", 2, true, "rolled back by testC, must not survive")); long duringTxn = session.createQuery("select count(w) from TestDbWidget w", Long.class).getSingleResult(); session.getTransaction().rollback(); session.close(); DEMO.info("testC saw {} row(s) DURING its own transaction (includes its own uncommitted insert), then rolled back", duringTxn); } @Test @Order(4) void testD_doesNotSeeTestCsRolledBackRow() { Session session = pollutedSf.openSession(); long count = session.createQuery( "select count(w) from TestDbWidget w where w.sku = :sku", Long.class) .setParameter("sku", "BBBBB") .getSingleResult(); session.close(); DEMO.info("testD looks specifically for testC's SKU 'BBBBB' -- found {} (must be 0: rollback isolated it)", count); assertThat(count).isZero(); } }