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.