From 208308702f5f4aa5b0b4e8884ee885bfe6b5cb8a Mon Sep 17 00:00:00 2001 From: Scott Leberknight <174812+sleberknight@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:57:44 +0000 Subject: [PATCH] Add JpaTestHelper * This is a utility for testing JPA-based DAOs Closes #440 --- pom.xml | 17 ++- .../test/hibernate/HibernateTestHelper.java | 2 +- .../jakarta/persistence/JpaTestHelper.java | 117 ++++++++++++++++++ .../persistence/JpaTestHelperTest.java | 55 ++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/kiwiproject/test/jakarta/persistence/JpaTestHelper.java create mode 100644 src/test/java/org/kiwiproject/test/jakarta/persistence/JpaTestHelperTest.java diff --git a/pom.xml b/pom.xml index 295763be..27fedfea 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,9 @@ 3.2.0 2.0.6 + + 3.1.0 + kiwiproject_kiwi-test kiwiproject @@ -46,12 +49,18 @@ pom import - + org.kiwiproject kiwi ${kiwi.version} + + + jakarta.persistence + jakarta.persistence-api + ${jakarta.persistence-api.version} + @@ -143,6 +152,12 @@ provided + + jakarta.persistence + jakarta.persistence-api + provided + + org.jdbi jdbi3-postgres diff --git a/src/main/java/org/kiwiproject/test/hibernate/HibernateTestHelper.java b/src/main/java/org/kiwiproject/test/hibernate/HibernateTestHelper.java index 56b56e62..626905ac 100644 --- a/src/main/java/org/kiwiproject/test/hibernate/HibernateTestHelper.java +++ b/src/main/java/org/kiwiproject/test/hibernate/HibernateTestHelper.java @@ -7,7 +7,7 @@ import org.hibernate.SessionFactory; /** - * Test utility for testing Hibernate-based code. This really makes sense when your tests are using a framework + * Test utility for testing Hibernate-based code. This is mainly useful when your tests are using a framework * that sets up a transactions before each test, executes the tests inside that transaction, and then rolls the * transactions back after each test. The methods here are useful to flush and clear the Hibernate {@link Session} * during test execution, otherwise Hibernate won't always automatically flush. In addition, you want to generally diff --git a/src/main/java/org/kiwiproject/test/jakarta/persistence/JpaTestHelper.java b/src/main/java/org/kiwiproject/test/jakarta/persistence/JpaTestHelper.java new file mode 100644 index 00000000..3c672f75 --- /dev/null +++ b/src/main/java/org/kiwiproject/test/jakarta/persistence/JpaTestHelper.java @@ -0,0 +1,117 @@ +package org.kiwiproject.test.jakarta.persistence; + +import static org.kiwiproject.base.KiwiPreconditions.requireNotNull; + +import com.google.common.annotations.Beta; + +import lombok.Getter; + +import jakarta.persistence.EntityManager; + +/** + * Test utility for testing JPA-based code. This mainly makes sense when your tests are using a framework + * that sets up a transaction before each test, executes the test inside that transaction, and then rolls + * back the transaction after the test. The methods here are useful to flush and clear the {@link EntityManager} + * during test execution, otherwise JPA (e.g., Hibernate) won't always automatically flush. In addition, + * you want to generally clear the {@link EntityManager} before performing certain operations to ensure + * the cache is cleared out, for example after inserting test data but before performing a query to ensure + * the test data is returned. + *

+ * 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); + } +}