【问题标题】:How to test Hibernate criteria queries without using any database?如何在不使用任何数据库的情况下测试 Hibernate 条件查询?
【发布时间】:2012-02-09 13:56:09
【问题描述】:

我正在开发一个包含许多复杂 Hibernate 条件查询的 Java 应用程序。我想测试这些标准,以确保他们选择了正确的,并且只选择了正确的对象。当然,解决这个问题的一种方法是建立一个内存数据库(例如 HSQL),并在每次测试中使用标准来回访问该数据库,然后断言查询结果符合我的期望。

但我正在寻找一个更简单的解决方案,因为 Hibernate 标准只是关于 Java 对象的一种特殊逻辑谓词。因此,理论上,它们可以在根本不访问任何数据库的情况下进行测试。例如,假设有一个实体叫Cat

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

我想做这样的事情来创建条件查询:

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)))

标准可以像这样在生产代码中使用:

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

是否有任何 Java 库使这种测试成为可能?

【问题讨论】:

    标签: unit-testing hibernate-criteria


    【解决方案1】:

    您可以使用 Mockito 之类的模拟框架来模拟所有相关的 Hibernate 类并定义这些模拟的预期行为。

    听起来像是很多代码,但由于 Hibernate Criteria API 是一个流畅的接口,Criteria 的所有方法都返回一个新实例 Criteria。所以定义所有测试通用的模拟行为很简单。 这是一个使用 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);
    }
    

    Criteria mock 的所有方法再次返回 mock。

    在具体测试中,您将使用ArgumentCaptorverify 语句来调查模拟的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());
    }
    

    我看到这种单元测试的问题是它们对被测类强加了很多期望。如果你想到serviceUnderTest 作为一个黑盒,您无法知道它是如何通过名称检索 cat 对象的。它还可以使用LIKE 标准甚至“IN”而不是=,进一步它可以使用 Example 标准。或者它可以执行原生 SQL 查询。

    【讨论】:

    • 感谢您的回答!但是,正如您所指出的,这种测试验证类是如何实现,而不是它应该如何表现。另外,这个测试集中在另一个服务上,它恰好使用了这个标准。虽然这肯定是一个重要的功能,但它应该相对容易测试(通过使用正确的接口解耦类、职责分离等)。在这里,我更感兴趣的是测试 Criteria 对象本身。
    • assertEquals 将检查对象是否相等而不是值或属性
    • @Subramanya 嗯,这取决于equals()在相应类中的实现。
    • @rainer198 是的,如果 equals 方法是否正在实施。
    【解决方案2】:

    我认为,您必须在此处与 H2 或其他内存数据库进行集成测试。正如您所说,如果您只使用模拟,您可以看到对象之间如何交互,但您永远不知道您会得到什么结果列表。

    我在同一页面上,不是 Restriction 左右,而是 JPA 2.0 CriteriaQueryCriteriaBuilder。我在持久层中构建了复杂的谓词,最后,我发现使用 db 中的数据进行测试变得不可避免,因为没有人知道 SQL 中的最终查询是什么。我决定在系统的这一部分需要集成,所以我就去做了。

    最后构建这样的测试并不难。你需要 H2 依赖,像这样的persistence.xml

    <?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>
    
    

    (请注意上面XML中的注释,我花了一周的时间终于解决了)

    关于提供者的注意事项:请参阅此处:How to configure JPA for testing in Maven

    而在两个sql文件中,你CREATE TABLE ...INSERT INTO ...。插入您喜欢的任何内容,因为数据是测试的一部分。

    还有,像这样的测试:

    /**
     * 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");
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-05-08
      • 2016-01-23
      • 2012-04-03
      • 1970-01-01
      • 2021-08-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多