JPA Unit Test across Multiple Providers and Databases
Published: 2020-02-16
Updated: 2020-02-23
Web: https://fritzthecat-blog.blogspot.com/2020/02/jpa-unit-test-across-multiple-providers.html
If you intend to test JPA functionality with several JPA-providers and databases, you may be interested in this article. It introduces a much more sophisticated test-framework than what I used in my previous JPA-Blogs.
CAUTION: the test abstraction shown in the following will clear all involved database tables completely, and on startup it will drop the database schema and create a new one. To restrict the cleanup to a set of records instead of a set of tables you need to override AbstractJpaTest.clearDatabase()
. So be careful when running such a test on your database!
Specification
Aim is to run a test on several database / provider combinations. Actually JPA should shield you from database specifics, but reality shows that not everything runs smoothly (e.g. Postgres problems with UUID primary keys).
Let's say you want to run a unit test on H2 and Postgres databases (2), using Hibernate and EclipseLink providers (2), then you have 4 (2 * 2) executions per test method:
- testXxx()
- Hibernate on H2
- Hibernate on Postgres
- EclipseLink on H2
- EclipseLink on Postgres
All those executions should perform isolated from each other, that means no state and no data should be left by a unit test that could affect the result of another. The entity classes (database tables) should be managed by a test abstraction that creates EntityManager
instances and cleans up the database after the test ran.
No persistence.xml
should be involved, any unit test class declares its entity classes at runtime, without using any component-scan for annotated @Entity
classes. Thus any test class could work on different database tables (although all its methods have to use the same set of tables).
Concept
The JPA persistence.xml
mixes together things I want to have separated, to be able to combine them at runtime:
- provider information (provider class name, specific behavior like
@Entity
autodetection, SQL logging, ...) - database properties (JDBC connection and driver information, SQL dialect)
- entity classes (the
<class>
elements, naming the managed classes)
Key is to not use Persistence
to create an EntityManagerFactory
, because it doesn't allow to configure entity classes (except through persistence.xml
). Instead search PersistenceProvider
implementations at runtime, using ServiceLoader.load()
. This offers a createContainerEntityManagerFactory()
method that accepts a PersistenceUnitInfo
object as first parameter, representing persistence.xml
. The optional second parameter can be used to define database properties, they will override the ones in PersistenceUnitInfo
. Mind that createContainerEntityManagerFactory()
will not read persistence.xml
. When you call the method with a PersistenceUnitInfo
that doesn't match the PersistenceProvider
's class, it will return null, thus you always can return the first not-null factory (this will fail only if two PersistenceUnitInfo
instances reference the same provider).
Thus a test abstraction would require
- a set of
PersistenceUnitInfo
objects, each holding information of one JPA provider (Hibernate, EclipseLink) - a set of database
Properties
objects - a list of entity classes annotated with
@Entity
to perform tests with.
/**
* @param managedClasses the classes representing the involved database tables.
* @return all JPA persistence-units to use for tests.
*/
protected abstract PersistenceUnitInfo[] getPersistenceUnits(List<Class<?>> managedClasses);
/**
* @param persistenceUnitName the name of the unit that will access the returned databases.
* @return a set of Properties objects, each object specifying JDBC properties of a test database,
* or null for falling back to persistence-unit properties.
*/
protected abstract Properties[] getDatabasePropertiesSets(String persistenceUnitName);
/** @return the classes representing the involved database tables, in removal-order. */
protected abstract List<Class<?>> getManagedClasses();
The abstraction could combine providers with databases, and allocate an EntityManager
for each combination. The list of classes is the same for all combinations.
The abstraction requires the test to wrap its code into a lambda. This lambda will be called as many times as provider / database combinations exist:
@Test
public void persistingACityShouldCascadeToItsHouses() {
executeForAll((EntityManager entityManager) -> {
// build test data
// perform test action
// assert result
});
}
The executeForAll()
method is implemented in super-class (abstraction). The lambda receives an EntityManager
as parameter.
Drawbacks
- JUnit
AssertionError
and exceptions can not be thrown immediately, because then other providers / databases would not be tested. Thus errors and exceptions are collected via Throwable.addSuppressed(exception)
, and get thrown after all combinations have tried. - Each test method must wrap its code into a lambda, and call
executeForAll()
of the super-class. - For convenience and logging, every test method should define a test-name for its lambda, thus it must duplicate its own name.
- A unit test class defines the tables for all of its test methods, a method can decide to use less than defined, but not more.
Prerequisites
What you need on your machine is at least two different database products. I installed Postgres and H2 (an easy-to-use platform-independent Java database). And you need to know how to write JDBC connection properties for them.
Maven Dependencies
Embed the following in a Maven pom.xml
, build the project and import it into your preferred IDE (click to expand):
<!-- JPA providers -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.4.4.Final</version>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.7.5</version>
</dependency>
<!-- JDBC drivers -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.199</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.9</version>
</dependency>
<!-- Test scope -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13-beta-3</version>
<scope>test</scope>
</dependency>
This declares, besides the necessary JDBC drivers, both Hibernate and EclipseLink as JPA-providers.
Application Classes
I placed these classes in src/main/java
directory, because they could be useful also for an application.
JPA Utility
Here are some JPA convenience implementations (click to expand):
JpaUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction;
public final class JpaUtil { /** * @param <T> the class of the objects in returned result list. * @param persistenceClass the database table to be queried. * @param em JPA database access. * @return all records from given table. */ public static <T> List<T> findAll(Class<T> persistenceClass, EntityManager em) { return em .createQuery( "select x from "+persistenceClass.getName()+" x", persistenceClass) .getResultList(); }
/** * @param persistenceClass the database table to be queried. * @param em JPA database access. * @return the number of records in given table. */ public static Long countAll(Class<?> persistenceClass, EntityManager em) { return em .createQuery( "select count(x) from "+persistenceClass.getName()+" x", Long.class) .getSingleResult(); } /** * @param entities the objects to be persisted, in order. * @param em JPA database access. */ public static void persistAll(Object[] entities, EntityManager em) { JpaUtil.transactional( em, (toPersist) -> { for (Object entity : toPersist) em.persist(entity); }, entities ); }
/** * @param entities the entities to remove from database, in order. * @param em JPA database access. */ public static void removeAll(Object[] entities, EntityManager em) { JpaUtil.transactional( em, (toRemove) -> { for (Object entity : toRemove) em.remove(entity); }, entities ); }
/** * @param persistenceClasses the database tables to be cleared, in order. * @param em JPA database access. */ public static void clearAll(List<Class<?>> persistenceClasses, EntityManager em) { clearAll(persistenceClasses.toArray(new Class<?>[persistenceClasses.size()]), em); } /** * @param persistenceClasses the database tables to be cleared, in order. * @param em JPA database access. */ public static void clearAll(Class<?>[] persistenceClasses, EntityManager em) { JpaUtil.transactional( em, (entityTypes) -> { for (Class<?> entityType : entityTypes) for (Object entity : findAll(entityType, em)) em.remove(entity); }, persistenceClasses ); }
/** * @param entityManager required, for getting a transaction. * @param entityManagerFunction required, the persistence-function to call. * @param parameter optional, the parameter to pass to given function. */ public static <P> void transactional( EntityManager entityManager, Consumer<P> entityManagerFunction, P parameter) { transactionalResult( entityManager, p -> { entityManagerFunction.accept(p); return null; }, parameter); } /** * @param entityManager required, for getting a transaction. * @param entityManagerFunction required, the persistence-function to call. * @param parameter optional, the parameter to pass to given function. * @return the return value of the called function. */ public static <R,P> R transactionalResult( EntityManager entityManager, Function<P,R> entityManagerFunction, P parameter) { final EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); final R returnValue = entityManagerFunction.apply(parameter); transaction.commit(); return returnValue; } catch (Throwable th) { if (transaction.isActive()) transaction.rollback(); throw th; } } private JpaUtil() {} // do not instantiate }
|
Persistence Units
Following is a default implementation for the JPA PersistenceUnitInfo
interface that represents what normally is in persistence.xml
. It is the base class for HibernatePersistenceUnit
and EclipselinkPersistenceUnit
:
DefaultPersistenceUnit.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | import java.net.URL; import java.util.*; import java.util.stream.Collectors; import javax.persistence.SharedCacheMode; import javax.persistence.ValidationMode; import javax.persistence.spi.ClassTransformer; import javax.persistence.spi.PersistenceUnitInfo; import javax.persistence.spi.PersistenceUnitTransactionType; import javax.sql.DataSource;
public class DefaultPersistenceUnit implements PersistenceUnitInfo { private final String persistenceUnitName; private final String providerClassName; private PersistenceUnitTransactionType transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; private Properties properties = new Properties(); private List<String> managedClasses = new ArrayList<>(); public DefaultPersistenceUnit( String persistenceUnitName, String providerClassName, Properties properties, List<Class<?>> managedClasses, PersistenceUnitTransactionType transactionType) { if (persistenceUnitName == null || providerClassName== null) throw new IllegalArgumentException("persistenceUnitName and providerClassName must not be null!"); this.persistenceUnitName = persistenceUnitName; this.providerClassName = providerClassName; if (properties != null) this.properties = properties; if (managedClasses != null) this.managedClasses = managedClasses.stream() .map(clazz -> clazz.getName()) .collect(Collectors.toList());
if (transactionType != null) this.transactionType = transactionType; } @Override public String getPersistenceUnitName() { return persistenceUnitName; }
@Override public String getPersistenceProviderClassName() { return providerClassName; }
@Override public List<String> getManagedClassNames() { return managedClasses; }
@Override public boolean excludeUnlistedClasses() { return managedClasses.isEmpty() == false; }
@Override public Properties getProperties() { return properties; }
@Override public PersistenceUnitTransactionType getTransactionType() { return transactionType; }
@Override public URL getPersistenceUnitRootUrl() { return getClass().getResource("/"); }
@Override public ClassLoader getClassLoader() { return Thread.currentThread().getContextClassLoader(); }
@Override public ClassLoader getNewTempClassLoader() { return getClassLoader(); } @Override public DataSource getJtaDataSource() { return null; } @Override public DataSource getNonJtaDataSource() { return null; } @Override public List<String> getMappingFileNames() { return Collections.emptyList(); } @Override public List<URL> getJarFileUrls() { return Collections.emptyList(); } @Override public SharedCacheMode getSharedCacheMode() { return null; } @Override public ValidationMode getValidationMode() { return null; } @Override public String getPersistenceXMLSchemaVersion() { return null; } @Override public void addTransformer(ClassTransformer transformer) { } }
|
Here are the derivates, defining those managed classes that they receive in constructor. This constructor parameter enables us to vary managed classes per test-class.
import java.util.Properties;
import java.util.List;
public class HibernatePersistenceUnit extends DefaultPersistenceUnit
{
public static final String NAME = "HibernateTestPU";
private static final Properties properties = new Properties();
static {
properties.put("hibernate.archive.autodetection", "none");
properties.put("hibernate.show_sql", "true");
}
public HibernatePersistenceUnit(List<Class<?>> managedClasses) {
super(
NAME,
"org.hibernate.jpa.HibernatePersistenceProvider",
properties,
managedClasses,
null);
}
}
import java.util.Properties;
import java.util.List;
public class EclipselinkPersistenceUnit extends DefaultPersistenceUnit
{
public static final String NAME = "EclipselinkTestPU";
private static final Properties properties = new Properties();
static {
properties.put("eclipselink.logging.level.sql", "FINE");
properties.put("eclipselink.logging.parameters", "true");
}
public EclipselinkPersistenceUnit(List<Class<?>> managedClasses) {
super(
NAME,
"org.eclipse.persistence.jpa.PersistenceProvider",
properties,
managedClasses,
null);
}
}
Most important is the fully-qualified class name of the PersistenceProvider
implementation, and the name of the persistence-unit, made public for database properties that depend on it.
Database Properties
The database properties must refer to the persistence units to decide the SQL dialect. As you can see below, they do this using the public constants EclipselinkPersistenceUnit.NAME
and HibernatePersistenceUnit.NAME
.
import java.util.Properties;
public class PostgresProperties extends Properties
{
public PostgresProperties(String persistenceUnitName) {
put("javax.persistence.jdbc.url", "jdbc:postgresql://localhost/template1");
put("javax.persistence.jdbc.driver", "org.postgresql.Driver");
put("javax.persistence.jdbc.user", "postgres");
put("javax.persistence.jdbc.password", "postgres");
if (persistenceUnitName.equals(EclipselinkPersistenceUnit.NAME))
put("eclipselink.target-database", "PostgreSQL");
else if (persistenceUnitName.equals(HibernatePersistenceUnit.NAME))
put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
}
}
import java.util.Properties;
public class H2Properties extends Properties
{
public H2Properties(String persistenceUnitName) {
put("javax.persistence.jdbc.url", "jdbc:h2:tcp://localhost/~/test");
put("javax.persistence.jdbc.driver", "org.h2.Driver");
put("javax.persistence.jdbc.user", "sa");
put("javax.persistence.jdbc.password", "");
if (persistenceUnitName.equals(EclipselinkPersistenceUnit.NAME))
put("eclipselink.target-database", "HSQL");
else if (persistenceUnitName.equals(HibernatePersistenceUnit.NAME))
put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
}
}
Further application classes are City
and House
, used by the example test on bottom. Please fetch them from my recent Blog about JOIN-types.
Test Classes
Following classes should be located in the src/test/java
directory.
Test Abstraction
Here is the test abstraction that implements all the things mentioned in chapter "Concepts".
AbstractJpaTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | import static org.junit.Assert.*; import java.util.*; import java.util.function.Consumer; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.spi.PersistenceProvider; import javax.persistence.spi.PersistenceUnitInfo; import org.junit.Before;
public abstract class AbstractJpaTest { private static Set<PersistenceProvider> providers; /** @return all JPA providers in CLASSPATH, cached statically. */ private static PersistenceProvider[] getPersistenceProviders() { if (providers == null) { providers = new HashSet<>(); for (PersistenceProvider persistenceProvider : ServiceLoader.load(PersistenceProvider.class)) providers.add(persistenceProvider); } return providers.toArray(new PersistenceProvider[providers.size()]); } private List<EntityManager> entityManagers = new ArrayList<>();
/** Executed before each test. Creates EntityManager instances for all provider / database combinations. */ @Before public void setUp() { entityManagers.clear(); for (final PersistenceUnitInfo persistenceUnit : getPersistenceUnits(getManagedClasses())) { final Properties[] databasePropertiesSets = getDatabasePropertiesSets(persistenceUnit.getPersistenceUnitName()); if (databasePropertiesSets == null || databasePropertiesSets.length <= 0) addEntityManager(persistenceUnit, null); else for (final Properties databaseProperties : databasePropertiesSets) addEntityManager(persistenceUnit, databaseProperties); } assertTrue(entityManagers.size() >= 1); }
/** * @param managedClasses the classes representing the involved database tables. * @return all JPA persistence-units to use for tests. */ protected abstract PersistenceUnitInfo[] getPersistenceUnits(List<Class<?>> managedClasses);
/** * @param persistenceUnitName the name of the unit that will access the returned databases. * @return a set of Properties objects, each object specifying JDBC properties of a test database, * or null for falling back to persistence-unit properties. */ protected abstract Properties[] getDatabasePropertiesSets(String persistenceUnitName);
/** @return the classes representing the involved database tables, in removal-order. */ protected abstract List<Class<?>> getManagedClasses(); /** * Override for another action than "drop-and-create". * @return the <i>javax.persistence.schema-generation.database.action</i>, * for test setup, one of "none", "create", "drop", "drop-and-create". */ protected String getDatabaseSetupCommand(String persistenceUnitName) { return "drop-and-create"; } /** * This executes given test on all EntityManager instances * and cleans up the database after each execution. * @param test required, the lambda that receives an EntityManager and executes the test. */ protected final void executeForAll(Consumer<EntityManager> test) { executeForAll(null, test); } /** * This executes given test on all EntityManager instances * and cleans up the database after each execution. * @param testName optional, name of the test method. * @param test required, the lambda that receives an EntityManager and executes the test. */ protected final void executeForAll(String testName, Consumer<EntityManager> test) { testName = (testName != null) ? testName : "test"; AssertionError error = null; Exception exception = null;
for (EntityManager entityManager : entityManagers) { final EntityManagerFactory factory = entityManager.getEntityManagerFactory(); final String databaseAndProviderInfo = bannerBegin(testName, factory, entityManager.getClass()); Throwable fail = null; try { test.accept(entityManager); } catch (Exception e) { // catch any test-exception and give others a try fail = e; exception = exceptionPreservation(e, databaseAndProviderInfo, exception); } catch (AssertionError e) { // catch any assertion and give others a try fail = e; error = assertionPreservation(e, databaseAndProviderInfo, error); } finally { bannerEnd(testName+" "+databaseAndProviderInfo, fail); tearDown(entityManager, factory); } } if (exception != null) // prefer throwing exceptions to JUnit container throw (exception instanceof RuntimeException) ? (RuntimeException) exception : new RuntimeException(exception); if (error != null) // else throw first Assert.fail() to JUnit container throw error; }
private void tearDown(EntityManager entityManager, EntityManagerFactory factory) { if (entityManager.isOpen() == false) // test could have closed it entityManager = factory.createEntityManager(); try { clearDatabase(entityManager); // this is the tear-down } finally { entityManager.close(); } } /** * Executed after each test run. Removes all records from all involved database tables * and closes the EntityManager. This calls <code>getManagedClasses()</code> and relies * on the dependency-order of it. Override for a more specific database-cleanup. * @param entityManager not null, the entityManager to use for cleanup. */ protected void clearDatabase(EntityManager entityManager) { final List<Class<?>> managedClasses = getManagedClasses(); JpaUtil.clearAll(managedClasses, entityManager); for (final Class<?> persistenceClass : managedClasses) { // make sure it worked final List<?> all = JpaUtil.findAll(persistenceClass, entityManager); assertEquals(persistenceClass.getSimpleName()+" "+all, 0, all.size()); // check order of persistence classes when this fails! } }
private Exception exceptionPreservation(Exception newException, String databaseAndProviderInfo, Exception existingException) { newException.addSuppressed(new RuntimeException(databaseAndProviderInfo)); if (existingException == null) existingException = newException; else existingException.addSuppressed(newException); return existingException; } private AssertionError assertionPreservation(AssertionError newError, String databaseAndProviderInfo, AssertionError existingError) { final AssertionError wrapper = new AssertionError(newError.getMessage()+", "+databaseAndProviderInfo); wrapper.setStackTrace(newError.getStackTrace()); if (existingError == null) existingError = wrapper; else existingError.addSuppressed(wrapper); return existingError; } private void addEntityManager(PersistenceUnitInfo persistenceUnit, Properties databaseProperties) { databaseProperties = ensureSchemaGenerationByFactory(persistenceUnit, databaseProperties); for (PersistenceProvider persistenceProvider : getPersistenceProviders()) { if (persistenceProvider.getClass().getName().equals(persistenceUnit.getPersistenceProviderClassName())) { final EntityManagerFactory factory = persistenceProvider.createContainerEntityManagerFactory( persistenceUnit, databaseProperties); if (factory != null) { entityManagers.add(factory.createEntityManager()); return; } } } throw new IllegalArgumentException("No EntityManagerFactory found for persistence-unit "+persistenceUnit.getPersistenceUnitName()); }
private Properties ensureSchemaGenerationByFactory(PersistenceUnitInfo persistenceUnit, Properties databaseProperties) { if (databaseProperties == null) databaseProperties = new Properties(); final String DATABASE_ACTION = "javax.persistence.schema-generation.database.action"; if (persistenceUnit.getProperties().getProperty(DATABASE_ACTION) == null && databaseProperties.getProperty(DATABASE_ACTION) == null) databaseProperties.setProperty(DATABASE_ACTION, getDatabaseSetupCommand(persistenceUnit.getPersistenceUnitName())); // without this, database schema would not be generated by // PersistenceProvider.createContainerEntityManagerFactory() return databaseProperties; }
private String bannerBegin(String testName, EntityManagerFactory factory, Class<?> entityManagerClass) { final String databaseUrl = ""+factory.getProperties().get("javax.persistence.jdbc.url"); final String where = "on "+databaseUrl+" with "+entityManagerClass.getName();
System.out.println("=========================================="); System.out.println("Executing "+testName+" "+where); System.out.println("------------------------------------------"); return where; }
private void bannerEnd(final String title, Throwable fail) { System.out.println("------------------------------------------"); System.out.println((fail != null ? "Crashed " : "Finished ")+title+(fail != null ? (": "+fail) : "")); System.out.println("=========================================="); } }
|
Following two utility classes derive AbstractJpaTest
and combine it with application classes, thus satisfying the test requirements. For every database you want to test, MultiJpaDatabaseTest
needs to provide a Properties
object. The MultiJpaProviderTest
most likely won't need maintenance, except when you intend to include DataNucleus and/or OpenJpa into your tests.
import java.util.List;
import javax.persistence.spi.PersistenceUnitInfo;
import fri.jpa.configuration.EclipselinkPersistenceUnit;
import fri.jpa.configuration.HibernatePersistenceUnit;
public abstract class MultiJpaProviderTest extends AbstractJpaTest
{
@Override
protected PersistenceUnitInfo[] getPersistenceUnits(List<Class<?>> managedClasses) {
return new PersistenceUnitInfo[] {
new EclipselinkPersistenceUnit(managedClasses),
new HibernatePersistenceUnit(managedClasses),
};
}
}
This narrowed AbstractJpaTest
to use the two most prominent JPA providers.
import java.util.Properties;
import fri.jpa.configuration.H2Properties;
import fri.jpa.configuration.PostgresProperties;
public abstract class MultiJpaDatabaseTest extends MultiJpaProviderTest
{
@Override
protected Properties[] getDatabasePropertiesSets(String persistenceUnitName) {
return new Properties[] {
new H2Properties(persistenceUnitName),
new PostgresProperties(persistenceUnitName)
};
}
}
This class narrowed MultiJpaProviderTest
to use H2 and Postgres as databases.
A concrete test example follows. Mind that the test defines its entity classes in managedClasses
on top. This satifies the last test abstraction requirement.
CityTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | import static org.junit.Assert.*; import java.util.*; import org.junit.Test;
public class CityTest extends MultiJpaDatabaseTest { private static final List<Class<?>> managedClasses = new ArrayList<>(); static { managedClasses.add(City.class); managedClasses.add(House.class); // This order is needed! // Hibernate would not delete a House that is still // referenced by a city through its @OneToMany Collection! } @Override protected List<Class<?>> getManagedClasses() { return managedClasses; } @Test public void persistingACityShouldCascadeToItsHouses() { executeForAll("persistingACityShouldCascadeToItsHouses", (entityManager) -> { // build test data final String WASHINGTON = "Washington"; final String PENTAGON = "Pentagon"; final String WHITEHOUSE = "White House"; final City washington = newCity(WASHINGTON, new String[] { PENTAGON, WHITEHOUSE }); // perform test action JpaUtil.transactional(entityManager, entityManager::persist, washington); // assert result final List<City> persistentCities = JpaUtil.findAll(City.class, entityManager); assertEquals(1, persistentCities.size()); final City persistentWashington = persistentCities.get(0); assertEquals(WASHINGTON, persistentWashington.getName()); final Set<House> persistentHouses = persistentWashington.getHouses(); assertEquals(2, persistentHouses.size()); assertTrue(persistentHouses.stream() .filter(h -> h.getName().equals(PENTAGON)) .findFirst().isPresent()); assertTrue(persistentHouses.stream() .filter(h -> h.getName().equals(WHITEHOUSE)) .findFirst().isPresent()); }); } private House newHouse(String name) { final House house = new House(); house.setName(name); return house; }
private City newCity(String cityName, String[] houseNames) { final City city = new City(); city.setName(cityName); for (String houseName : houseNames) city.getHouses().add(newHouse(houseName)); return city; } }
|
You could easily restrict the test to use just EclipseLink by overriding getPersistenceUnits(List managedClasses)
, or exclude a database by overriding getDatabasePropertiesSets(String persistenceUnitName)
.
Following is the test's output (besides provider logging):
==========================================
Executing persistingACityShouldCascadeToItsHouses on jdbc:h2:tcp://localhost/~/test with org.eclipse.persistence.internal.jpa.EntityManagerImpl
------------------------------------------
....
------------------------------------------
Finished persistingACityShouldCascadeToItsHouses on jdbc:h2:tcp://localhost/~/test with org.eclipse.persistence.internal.jpa.EntityManagerImpl
==========================================
....
==========================================
Executing persistingACityShouldCascadeToItsHouses on jdbc:postgresql://localhost/template1 with org.eclipse.persistence.internal.jpa.EntityManagerImpl
------------------------------------------
....
------------------------------------------
Finished persistingACityShouldCascadeToItsHouses on jdbc:postgresql://localhost/template1 with org.eclipse.persistence.internal.jpa.EntityManagerImpl
==========================================
....
==========================================
Executing persistingACityShouldCascadeToItsHouses on jdbc:h2:tcp://localhost/~/test with org.hibernate.internal.SessionImpl
------------------------------------------
....
------------------------------------------
Finished persistingACityShouldCascadeToItsHouses on jdbc:h2:tcp://localhost/~/test with org.hibernate.internal.SessionImpl
==========================================
....
==========================================
Executing persistingACityShouldCascadeToItsHouses on jdbc:postgresql://localhost/template1 with org.hibernate.internal.SessionImpl
------------------------------------------
....
------------------------------------------
Finished persistingACityShouldCascadeToItsHouses on jdbc:postgresql://localhost/template1 with org.hibernate.internal.SessionImpl
==========================================
Review
It was not easy to separate the informations inside persistence.xml
. I looked into the implementation of Persistence.java
to find out how I can circumvent it and use the PersistenceProvider
interface instead, because Persistence
doesn't allow to override the persistence.xml
configuration.
Finally I got a test that can detect differences between JPA providers, and problems occurring when changing the underlying database product. In my next Blog I will show an interesting difference between Hibernate and EclipseLink concerning the removal of an entity that is still referenced in an @OneToMany
collection.
ɔ⃝ Fritz Ritzberger, 2020-02-16