Blog-Archiv

Sonntag, 16. Februar 2020

JPA Unit Test across Multiple Providers and Databases

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()
    1. Hibernate on H2
    2. Hibernate on Postgres
    3. EclipseLink on H2
    4. 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:

  1. provider information (provider class name, specific behavior like @Entity autodetection, SQL logging, ...)
  2. database properties (JDBC connection and driver information, SQL dialect)
  3. 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

  1. a set of PersistenceUnitInfo objects, each holding information of one JPA provider (Hibernate, EclipseLink)
  2. a set of database Properties objects
  3. 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.




Keine Kommentare: