September 25, 2010

False Positives in Database Integration Tests

You may sometimes find that all your tests pass, but in production your entities cannot be hydrated from the database. Take this example:

@PersistenceContext(unitName="shipmentOrder")
EntityManager entityManager;

@Test
@Transactional
public void maps_correctly_to_and_from_database() throws Exception {

	ShipmentOrder state = new ShipmentOrder();
	state.setCustomerName("CALLERNAME");
	state.getDeliveryAddress().setCountry("NIRVANA");

	entityManager.persist(state);
	entityManager.flush(); // enforce writing to the database

	ShipmentOrder foundState = entityManger.find(ShipmentOrder.class, state.getId());

	assertEquals("CALLERNAME", foundState.getCustomerName());
	assertEquals("NIRVANA", foundState.getDeliveryAddress().getCountry());
}

This test will happily pass, but the same call fails in production. Why?

Below are the entity classes - note, that JPA by default does not allow @Embeddable's to inherit (see e.g. the Hibernate Annotations Reference, section "3.2.4.4. Inherit properties from superclasses" for more details). Even worse, Hibernate does not complain, but silently ignores inherited fields.

@Entity
class ShipmentOrder {
	String customerName;
	@Embedded
	DeliveryAddress deliveryAddress;

	String getCustomerName() { return customerName; }
	DeliveryAddress getDeliveryAddress() { return deliveryAddress; }
}

@Embeddable
class DeliveryAddress extends Address{
	String deliveryLocation;
	String getDeliveryLocation();
}

class Address {
	String country;
	String getCountry();
}

So, why does the test pass then?

The problem is, that while within a transaction, Hibernate reuses objects from the 1st Level cache. This means the our call to "flush()" causes Hibernate to write changes to the database, but the entity instance is still kept in memory. If we execute the find() method, Hibernate simply returns the entity instance from the cache and will never try to hydrate the instance from the database.

Solution

EntityManager.clear() allows to manually purge the 1st level cache. Changing the test to

repository.store(state);
entityManager.flush(); // enforce writing to the database

entityManager.clear(); // purge 1st level cache

state = repository.findById(state.getId());

will successfully catch the mapping error.

Here is a method we use in integration tests to automatically flush+purge all persistence contexts defined in Spring:

protected void flushAndClearPersistenceContexts() {
    Map<String, EntityManagerFactory> entityManagers = this.getApplicationContext().getBeansOfType(EntityManagerFactory.class);
    for(EntityManagerFactory emf:entityManagers.values()) {
        EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource(emf);
        if (emHolder != null) {
            final EntityManager entityManager = emHolder.getEntityManager();
            try {
                entityManager.flush();
            } catch (TransactionRequiredException e) {
                // ignore - couldn't find a way to properly check if we are within a transaction
            }
            entityManager.clear();
        }
    }
}

NOTE: never use flush() and clear() in your production code as this seriously kills the performance of hibernate

You can find the sample code reproducing this issue on my git repository.

2 comments:

wageoghe said...

Does anyone monitor the net-common mailing list? I have posted some questions about Common Logging over the past couple of months but have gotten any responses. If you have some time to spare, could you take a look at my messages and reply to the list or directly to me?

Thanks.

william dot geoghegan at intergraph dot com

wageoghe said...

That las message should say "... have not gotten any responses ..."