12

I'm developing a Java application with lots of complex Hibernate criteria queries. I would like to test these criteria to make sure they are selecting the right, and only the right, objects. One approach to this, of course, is to set up an in-memory database (e.g. HSQL) and, in each test, make a round trip to that database using the criteria and then assert that the query results match my expectations.

But I'm looking for a simpler solution, since Hibernate criteria are just a special kind of logical predicates about Java objects. Thus they could, in theory, be tested without accessing any database at all. For example, assuming that there is a entity called Cat:

class Cat {
    Cat(String name, Integer age){
        this.name = name;
        this.age = age;
    }
    ...
}

I would like to do something like this, to create criteria queries:

InMemoryCriteria criteria = InMemoryCriteria.forClass(Cat.class)
   .add(Restrictions.like("name", "Fritz%"))
   .add(Restrictions.or(
      Restrictions.eq("age", new Integer(0)),
      Restrictions.isNull("age")))

assertTrue(criteria.apply(new Cat("Foo", 0)))
assertTrue(criteria.apply(new Cat("Fritz Lang", 12)))
assertFalse(criteria.apply(new Cat("Foo", 12)))

The criteria could be used in production code like this:

criteria.getExecutableCriteria(session); //similar to DetachedCriteria

Is there any Java library that makes this kind of test possible?

Otavio Macedo
  • 1,542
  • 1
  • 13
  • 34

2 Answers2

14

You could use a mocking framework like Mockito to mock all relevant Hibernate classes and define expected behavior of these mocks.

Sounds like a lot of code, but since the Hibernate Criteria API is a fluent interface, all methods of Criteria return a new instance Criteria. So defining the mock behavior which is common to all tests is simple. Here is an example using Mockito

@Mock
private SessionFactory sessionFactory;

@Mock
Session session;

@Mock
Criteria criteria;

CatDao serviceUnderTest;

@Before
public void before()
{
    reset(sessionFactory, session, criteria);
    when(sessionFactory.getCurrentSession()).thenReturn(session);
    when(session.createCriteria(Cat.class)).thenReturn(criteria);
     when(criteria.setFetchMode(anyString(), (FetchMode) anyObject())).thenReturn(criteria);
    when(criteria.setFirstResult(anyInt())).thenReturn(criteria);
    when(criteria.setMaxResults(anyInt())).thenReturn(criteria);
    when(criteria.createAlias(anyString(), anyString())).thenReturn(criteria);
    when(criteria.add((Criterion) anyObject())).thenReturn(criteria);

    serviceUnderTest = new CatDao(sessionFactory);
}

All methods of the Criteria mock return the mock again.

In a concrete test you would then use a ArgumentCaptor and verify statements to investigate what happened to the mocked Criteria.

@Test
public void testGetCatByName()
{
    ArgumentCaptor<Criterion> captor = ArgumentCaptor.forClass(Criterion.class);

    serviceUnderTest.getCatByName("Tom");

    // ensure a call to criteria.add and record the argument the method call had
    verify(criteria).add(captor.capture());

    Criterion criterion = captor.getValue();

    Criterion expectation = Restrictions.eq("name", "Tom");

    // toString() because two instances seem never two be equal
    assertEquals(expectation.toString(), criterion.toString());
}

The problem I see with this kind of unitests is that they impose a lot of expectations about the class under test. If you think of serviceUnderTest as a blackbox, you can't know how it retrieves the cat object by name. It could also use a LIKE criterion or even 'IN' instead of =, further it could use the Example criterion. Or it could execute a native SQL query.

rainer198
  • 3,195
  • 2
  • 27
  • 42
  • 4
    Thank you for answering! But, as you point out, this kind of test verifies how the class is *implemented*, not how it is supposed to *behave*. Plus, this test is focused on another service, that happens to use the criteria. Although this is certainly an important piece of functionality, it should be relatively easy to test (by decoupling classes with the right use of interfaces, separation of responsibilities, etc). Here, I am more interested in testing the Criteria object itself. – Otavio Macedo Feb 23 '12 at 15:45
  • assertEquals will check if the object are equal not the values or properties – SGuru Jan 27 '20 at 16:51
  • @Subramanya Well, this depends on the implementaion of `equals()` in the corresponding class. – rainer198 Jan 28 '20 at 15:53
  • @rainer198 yes, if equals method is being implemented or not. – SGuru Jan 29 '20 at 15:54
0

I think, you must do an integration test here with H2 or other in-memory database. As you said, if you only use mocks, you can see how object interacts with each other, but you never know what result list you get.

I am on the same page, not with Restriction or so, but with JPA 2.0 CriteriaQuery and CriteriaBuilder. I build complex predicates in my persistence layer, and at last, I find it becomes inevitable to test with data in db, as no one knows what would be the final query in SQL. And I decide that in this part of the system, an integration is needed, so I went for it.

At last it is not very hard to build such a test. You need H2 dependency, a persistence.xml like this:

<?xml version="1.0" encoding="UTF-8"?>
<!-- For H2 database integration tests. -->
<!-- For each int test, define unique name PU in this file and include SQL files in different paths. -->
<persistence version="2.0"
             xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="test-item-history-service-bean" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>  <!-- mind here: must be this! cannot be JPA provider! -->
        <class>com.data.company.Company</class>
        <class>com.data.company.ItemHistory</class>
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        <properties>
            <property name="javax.persistence.jdbc.url"
                      value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=Oracle;INIT=RUNSCRIPT FROM 'src/test/resources/db/item-history/create.sql'\;RUNSCRIPT FROM 'src/test/resources/db/item-history/populate.sql'"/>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.id.new_generator_mappings" value="true"/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>  <!-- mind here! Can only be "update"! "create-drop" will prevent data insertion! -->
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.default_schema" value="main"/>
        </properties>
    </persistence-unit>
</persistence>

(Mind very carefully the comment in the XML above, it took me a week to finally solve them)

Note about the provider: see here: How to configure JPA for testing in Maven

And in the two sql files, you CREATE TABLE ... and INSERT INTO .... Insert whatever you like, as the data is part of the test.

And, a test like this:

/**
 * Integration tests with in-memory H2 DB. Created because:
 *  - In-memory DB are relatively cheap to create and destroy, so these tests are quick
 *  - When using {@link javax.persistence.criteria.CriteriaQuery}, we inevitably introduce complex perdicates'
 *  construction into persistence layer, which is a drawback of it, but we cannot trade it with repetitive queries
 *  per id, which is a performance issue, so we need to find a way to test it
 *  - JBehave tests are for the user story flows, here we only want to check with the complex queries, certain
 *  records are returned; performance can be verified in UAM.
 */

@RunWith(MockitoJUnitRunner.class)
public class ItemHistoryPersistenceServiceBeanDBIntegrationTest {
    private static EntityManagerFactory factory;
    private EntityManager realEntityManager;
    private ItemHistoryPersistenceServiceBean serviceBean;
    private Query<String> inputQuery;

    @BeforeClass
    public static void prepare() {
        factory = Persistence.createEntityManagerFactory("test-item-history-service-bean");
    }

    @Before
    public void setup() {
        realEntityManager = factory.createEntityManager();

        EntityManager spy = spy(realEntityManager);

        serviceBean = new ItemHistoryPersistenceServiceBean();

        try {
            // inject the real entity manager, instead of using mocks
            Field entityManagerField = serviceBean.getClass().getDeclaredField("entityManager");
            entityManagerField.setAccessible(true);
            entityManagerField.set(serviceBean, spy);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new AssertionError("should not reach here");
        }

        inputQuery = new Query<>();
        inputQuery.setObjectId("itemId");

    }

    @After
    public void teardown() {
        realEntityManager.close();
    }

    @Test
    public void findByIdAndToken_shouldReturnRecordsMatchingOnlyTokenFilter() {
        try {
            // when
            List<ItemHistory> actual = serviceBean.findByIdAndToken(inputQuery);

            // then
            assertEquals(2, actual.size());
            assertThat(actual.get(0).getItemPackageName(), anyOf(is("orgId 3.88"), is("orgId 3.99.3")));
            assertThat(actual.get(1).getItemPackageName(), anyOf(is("orgId 3.88"), is("orgId 3.99.3")));
        } catch (DataLookupException e) {
            throw new AssertionError("should not reach here");
        }
    }
}
WesternGun
  • 11,303
  • 6
  • 88
  • 157