DevOps Zone is brought to you in partnership with:

Amar has posted 2 posts at DZone. View Full User Profile

Effective Testing around Hibernate/ORM layer

04.14.2014
| 6847 views |
  • submit to reddit
As applications that use Hibernate or any ORM frameworks grow complex, lots of performance issues don't get enough attention until it is too late. This article accounts for SLA time as part of the junits and allows developers to think in terms of calculating the number of calls hibernate is going to make in the background. This allows for developers to deal with facts rather than assumptions. 

Assuming we are doing TDD, let's build up the TestCase first.
We are going to build up a typical API to create Person with bunch of addresses.
To understand how this works, we will create a buggy Service and include background Audit mechanism, to simulate abstraction to developer.
 
@RunWith(SpringJUnit4ClassRunner.class)
public class PersonServiceTest {
  @Autowired
  private PersonService personService;

  @Before
  public void insertData() {
    TestUtil.createTestData(personService);
  }

  @Before
  public void cleanup() {
    QueryCountHolder.clear();
  }

  @Test(timeout = 500)
  public void testAddressCount() {
    assertThat(QueryCountHolder.getGrandTotal().getTotalNumOfQuery(), is(0));
    assertThat(personService.getAddressCount("John"), is(2));
    assertThat(QueryCountHolder.getGrandTotal().getTotalNumOfQuery(), is(1));
  }

  @Test(timeout = 500)
  public void testUpdatePerson() throws Exception {

    assertThat(QueryCountHolder.getGrandTotal().getTotalNumOfQuery(), is(0));

    Person person = personService.getCustomerByName("John");
    assertThat(person.getGender(), CoreMatchers.nullValue());
    person.setGender("Male");

    QueryCountHolder.clear();
    personService.save(person);
    assertThat(QueryCountHolder.getGrandTotal().getTotalNumOfQuery(), is(3));

    person = personService.getCustomerByName("John");
    assertThat(person.getGender(), CoreMatchers.is("Male"));
  }
}

testAddressCount - Based on the service interface, this test case assume 1 sql is executed to get the count of addresses give a first name 
testUpdatePerson - We update the person gender, and assume 3 sqls are executed.
  1. To read the person
  2. Update gender
  3. Audit event to record gender change
Both the test fails 
testAddressCount java.lang.AssertionError:  Expected: is <1> got: <2>
testUpdatePerson java.lang.AssertionError:  Expected: is <3> got: <4>

Let's dive deep into the service and see what is wrong.  
@Service
public class PersonServiceImpl implements PersonService {
  @Autowired
  private PersonRepository repository;

  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public int getAddressCount(String firstName) {
    List<Person> list = repository.findByFirstNameLike(firstName);
    if (list == null) {
      return 0;
    }
    Person byFirstNameLike = list.get(0);
    return byFirstNameLike == null ? 0 : byFirstNameLike.getAddresses().size();
  }

  @Transactional(readOnly = false, propagation = Propagation.SUPPORTS)
  public Person save(Person person) {
    // un intended operation
    person.setLastName(person.getLastName() + " 1");
    return repository.save(person);
  }
}


As you can see in getAddressCount, instead of writing a custom hql, the developer did an mistake of reading object and doing a size on the list. The correct way of writing this method is to make use of custom query which does a count(*) on address table. 

Similarly with Save, the test expected to update Gender, but the service was doing something more than expected, triggering a unexpected audit record on last name. 

To demonstrate this article I have made use of datasource-proxy which is very good API to capture sqls that hibernate or any other ORM frameworks execute under the hood. 

Also, I feel like it is very good practice to use @Test(timeout) so we know if some one changes a method later on, and hinder the SLA.
AttachmentSize
EffectiveHibernateTesting_SRC.zip14.27 KB
Published at DZone with permission of its author, Amar Mattey.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)