+ * Make sure to join the existing transaction inside your test if necessary, and make sure the DAOs under + * test are using the same EntityManager! For example, inside a setup method: + *
+ * {@literal @}BeforeEach + * void setUp(@Autowired EntityManagerFactory entityManagerFactory) { + * entityManager = entityManagerFactory.createEntityManager(); + * jpaTestHelper = new JpaTestHelper(entityManager); + * + * personDao = new PersonDao(entityManagerFactory) { + * {@literal @}Override + * protected EntityManager entityManager() { + * return entityManager; + * } + * }; + * + * // additional setup + * } + *+ * The above assumes your DAO class has a {@code protected} method that tests can override and supply the + * EntityManager to use. The DAO methods must use the EntityManager returned by this method, otherwise the + * code being tested in the DAO and the tests will be using different EntityManager instances, which will + * not work. + *
+ * If you are using Spring with its {@code JpaTransactionManager}, then you can declare a bean of type + * {@link EntityManager} and defined it as: + *
+ * {@literal @}Bean + * public EntityManager sharedEntityManager(EntityManagerFactory entityManagerFactory) { + * return SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory); + * } + *+ * Then, you can inject this "shared" EntityManager directly into DAOs and tests and it will be + * automatically used by the DAO and test code without needing to do any of the setup code above. + */ +@Beta +public final class JpaTestHelper { + + /** + * Return the {@link EntityManager} this helper was created with. + */ + @Getter + private final EntityManager entityManager; + + /** + * Create a new helper with the specified entity manager. + * + * @param entityManager the {@link EntityManager} this helper will use + */ + public JpaTestHelper(EntityManager entityManager) { + this.entityManager = requireNotNull(entityManager); + } + + /** + * Delegates to {@link EntityManager#joinTransaction()}. + *
+ * You should call this if you are handling transactions manually so that tests and the DAOs + * all participate in the same transaction. If you are using another mechanism to handle transactions, + * for example using Spring and its Test Context infrastructure using a shared EntityManager, then + * you may not need to call this. + * + * @return this instance, which allows chaining after the constructor + */ + public JpaTestHelper joinTransaction() { + entityManager.joinTransaction(); + return this; + } + + /** + * Flush the entity manager, effectively forcing JPA to perform database operations. Since this + * helper assumes tests are executed in a transaction, the database operations are performed + * but not committed. + * + * @see EntityManager#flush() + */ + public void flushEntityManager() { + entityManager.flush(); + } + + /** + * Clears the entity manager. + * + * @see EntityManager#clear() + */ + public void clearEntityManager() { + entityManager.clear(); + } + + /** + * Flushes, then clears the entity manager. + * + * @see EntityManager#flush() + * @see EntityManager#clear() + */ + public void flushAndClearEntityManager() { + flushEntityManager(); + clearEntityManager(); + } +} diff --git a/src/test/java/org/kiwiproject/test/jakarta/persistence/JpaTestHelperTest.java b/src/test/java/org/kiwiproject/test/jakarta/persistence/JpaTestHelperTest.java new file mode 100644 index 00000000..d16c60f3 --- /dev/null +++ b/src/test/java/org/kiwiproject/test/jakarta/persistence/JpaTestHelperTest.java @@ -0,0 +1,55 @@ +package org.kiwiproject.test.jakarta.persistence; + +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.EntityManager; + +/** + * Yes, this is really just a "restating the obvious test" to ensure no one "accidentally" changes something + * without thinking about what they're doing. And yes, it uses mock objects instead of real JPA ones, since + * we only need to verify the expected method calls and assume JPA knows what it's doing. + */ +@DisplayName("JpaTestHelper") +public class JpaTestHelperTest { + + private JpaTestHelper helper; + private EntityManager entityManager; + + @BeforeEach + void setUp() { + entityManager = mock(EntityManager.class); + helper = new JpaTestHelper(entityManager); + } + + @Test + void shouldFlushEntityManager() { + helper.flushEntityManager(); + + verify(entityManager, only()).flush(); + } + + @Test + void shouldClearEntityManager() { + helper.clearEntityManager(); + + verify(entityManager, only()).clear(); + } + + @Test + void shouldFlushAndClearEntityManager() { + helper.flushAndClearEntityManager(); + + var inOrder = inOrder(entityManager); + inOrder.verify(entityManager).flush(); + inOrder.verify(entityManager).clear(); + verifyNoMoreInteractions(entityManager); + } +}