【问题标题】:Mock Hibernate entityManager with createQuery(), parameters and executeUpdate()使用 createQuery()、参数和 executeUpdate() 模拟 Hibernate entityManager
【发布时间】:2020-12-06 16:56:35
【问题描述】:

我是 Java 单元测试的新手;我有一个使用 Hibernate 与 MySQL 数据库交互的应用程序。

我有许多使用createQuery() 方法构建的查询,也带有参数,如下所示:

return this.entityManager.createQuery("from MyEntity m where param  = :param", MyEntity.class)
    .setParameter("param", param)
    .getSingleResult();

我想避免模拟对 entityManager 对象的所有后续调用,因为有时我会使用超过 5 个参数进行查询,而且模拟每个调用似乎并不方便。

同样的概念可以应用于Builder objects

编辑 1

我添加了一个我使用的具体示例(鉴于这不是管理异常的好方法,但不幸的是通常很安静):

public class MyService {
private EntityManager entityManager;

public MyEntity find(String field ) {
        try{
            return this.entityManager.createQuery("from MyEntity  c where c.field = :field ", MyEntity .class)
                    .setParameter("field ", field )
                    .getSingleResult();
        } catch (NoResultException e) {
            return null;
        } catch (NonUniqueResultException e) {
            logger.error("find", e);
            return null;
        }
    }
}

在这个例子中,考虑到entityManager 上的调用行为,我有不同的分支需要测试。然后我必须模拟那个调用的答案来测试这个方法的所有行。

我发现了什么

我发现如下:

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private EntityManager entityManager;

按预期工作。我可以模拟所有调用链。 但是

  1. 引自Javadoc of Mockito.RETURNS_DEEP_STUBS

警告:常规干净代码很少需要此功能!把它留给遗留代码。模拟一个模拟返回一个模拟,返回一个模拟,(...),返回一些有意义的暗示违反德墨忒耳法则或模拟一个值对象(一个众所周知的反模式)。

  1. 如果上一点还不够,下一点,后面几行,显然设置了一个很大的限制:

当链中包含的任何返回类型的方法不能被模拟时(例如:是原始类或最终类),此功能将不起作用。这是因为 java 类型系统。

第二点意味着如果我尝试以这种方式模拟方法executeUpdate(),它返回一个int,它会引发异常。

when(entityManager.createQuery(anyString())
                .setParameter(eq("param"), anyString())
                .executeUpdate())
.thenReturn(1);

这样我就无法测试与entityManager 的交互。

问题

  1. 我应该如何模拟entityManager 上的调用?在我看来,我必须一个一个地模拟每个方法。
  2. Answers.RETURNS_DEEP_STUBS用错了吗?如果没有,我该如何处理第二个例子?

【问题讨论】:

  • 我的建议是专门使用集成测试来测试与EntityManager(或任何其他外部低级API)交互的代码。尝试为单元测试模拟它是浪费时间,而且无论如何只能使用集成测试对查询进行可靠测试。如果您对EntityManager API 有一个抽象(例如Dao 对象),该层您可以轻松地模拟其他单元测试
  • 反问:为什么要先嘲笑实体经理?为什么不针对数据库进行测试,例如内存中的 h2 还是通过 testcontainers 启动的成熟数据库? “单元测试”中的“单元”不一定是“类”。
  • @crizzis 没有 dao。服务直接访问 entityManager。我将使用代码示例更新问题。请注意,这种模式用于数百个类、数千种方法、许多项目中,因此无法更改:)
  • @Turing85 因为如果我得到正确的单元测试,则不会涉及数据库或外部 api 调用或类似的东西。但我问是因为我必须学习,所以每条建议都是一个好的开始!
  • 尽管与数据库无关的想法很好,但在 99% 的情况下,您 - 以一种或另一种方式 - 绑定到特定的 DBMS(例如,索引未在 ANSI-SQL 中定义) 并且抛出的异常(至少是消息)在实现之间会有所不同。在这方面,我建议使用实际使用/将在生产中使用的 DBMS(es)测试持久层。当然,这是以用测试数据填充数据库为代价的。

标签: java hibernate junit mockito junit5


【解决方案1】:

不要模拟 JPA API,只需使用适当的测试数据编写集成测试,然后针对真实数据执行真实查询,看看是否一切正常。像testcontainers 这样的项目让上手变得非常容易。

【讨论】:

  • 感谢您的回答。那么我不应该在使用 entityManager 的地方进行单元测试吗?考虑到我们没有分割对数据库和服务逻辑的访问的 DAO。似乎我无法在与 entityManager 交互的所有地方进行单元测试(几乎在不同服务中的所有地方)。
  • 考虑到如果我们使用 JPA 而不仅仅是 Hibernate 情况会更好,是否可以模拟 entityManager 方法链以返回我需要对我的方法逻辑进行单元测试的内容?
  • 如果您在后台使用数据库,IMO 测试仍然可以被视为“单元测试”,因此只需使用数据库并针对该数据库执行查询。除非您尝试测试表示层,否则模拟数据层是没有意义的,即使那样我也不会模拟它。测试真实的东西,虽然它可能会慢一点(但没那么快),但总会给你更高的信心。
  • 好的,谢谢你的建议。如果我理解得很好,我应该对所有不与 database/entityManager 交互的方法进行单元测试,而在集成测试中,我会测试所有这样做的方法。对于 IT 来说,可以通过 API 测试服务层的一些方法,并在服务的 IT 类上测试其他方法,或者这不是一个好的模式?因为我会在不同的地方测试同一个类的方法,可能会造成混淆。
  • 很少有你测试的“真正的单元”。您总是认为一些事情是理所当然的,即您用于实施测试的事情。该单元通常也使用库,但它是否因此是集成测试?不。当您以这种方式查看时,单元测试和集成测试之间的区别有些模糊。我认为将测试分为快速和慢速类别会更合适。但请相信我,您可以非常快速地进行使用 DB 的测试,因此我个人认为它们大部分时间都是“单元测试”。
猜你喜欢
  • 2012-04-12
  • 1970-01-01
  • 2013-03-20
  • 2016-05-02
  • 1970-01-01
  • 1970-01-01
  • 2012-04-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多