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:

EntityManager entityManager;

public void maps_correctly_to_and_from_database() throws Exception {

	ShipmentOrder state = new ShipmentOrder();

	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 " Inherit properties from superclasses" for more details). Even worse, Hibernate does not complain, but silently ignores inherited fields.

class ShipmentOrder {
	String customerName;
	DeliveryAddress deliveryAddress;

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

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.


EntityManager.clear() allows to manually purge the 1st level cache. Changing the test to;
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 {
            } catch (TransactionRequiredException e) {
                // ignore - couldn't find a way to properly check if we are within a transaction

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.


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?


william dot geoghegan at intergraph dot com

wageoghe said...

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