【问题标题】:Spock interaction based test: too few invocation on a method基于 Spock 交互的测试:对方法的调用太少
【发布时间】:2018-06-23 22:44:22
【问题描述】:

我有一个非常简单的方法。它调用另一个软删除 API 密钥的方法,然后调用另一个方法创建一个新的并返回它。

Test 也在下面,它只是检查两个方法是否被正确调用。但是,尽管这两种方法仍然出现 0 调用错误。是什么导致了这个问题?

AuthApiKeyPair updateApiKeyPair(AuthApiKeyPair apiKeyPair, Boolean createNewKey) {

    AuthApiKeyPair newKeyPair

    if (createNewKey) {
        deleteApiKeyPair(apiKeyPair)

        //The key will be created with the same info as the previous key.
        newKeyPair = createApiKeyPair(apiKeyPair.label, apiKeyPair.accountMode, apiKeyPair.source)
    }

    newKeyPair
}

测试:

def "should soft delete key pair and create new one"() {
    setup:
    AuthApiKeyPair apiKeyPair = AuthApiKeyPair.build(acquirerId: 123, source: PaymentSource.BOARDING_API, label: 'Boarding API key')

    when:
    service.updateApiKeyPair(apiKeyPair, true)

    then:
    1 * service.deleteApiKeyPair(apiKeyPair)
    1 * service.createApiKeyPair(apiKeyPair.label, apiKeyPair.accountMode, apiKeyPair.source)
}

【问题讨论】:

  • 你想知道为什么会发生错误,但是你既没有发布错误信息,也没有发布关键元素service的定义。信息隐藏使这个问题成为一个问答节目,你期待帮助的人只能推测。所以请更新问题,至少提供service 的定义。

标签: unit-testing grails groovy spock


【解决方案1】:

如果您考虑一下您的测试,您会发现在最好的情况下,它测试的是 Spock 的模拟机制,而不是您的业务代码。您没有向我们展示您的测试规范的完整课程,但是根据您的场景,我们可以假设您的测试中的 service 只是一个模拟。如果这是真的,那么你不能指望这两个调用:

then:
1 * service.deleteApiKeyPair(apiKeyPair)
1 * service.createApiKeyPair(apiKeyPair.label, apiKeyPair.accountMode, apiKeyPair.source)

发生。仅仅因为模拟类不执行真正的方法。

我强烈建议您测试一个真实的类,而不是测试特定方法会导致什么样的调用,而是调用特定方法的预期(和确定性)结果是什么。您可以在when 子句中对真实对象执行service.updateApiKeyPair(apiKeyPair, true),然后您可以检查是否创建了新的API 密钥对(并保存在您使用的存储中)以及旧的密钥对是否不再存在。与仅检查调用相比,此类测试至少有一些好处:

  • 您可以随时更改 service.updateApiKeyPair() 的实现,只要它产生相同的结果,您的测试仍然有用(因为测试不会像调用测试那样限制您的实现),
  • 您测试实际行为而不是模拟库 - 有这个 轶事称 Mockito 是数百万个项目中测试次数最多的库。

当然,它可能需要进行一些设计更改。我猜测您的服务类使用了一些注入的 DAO 或存储 API 密钥对的存储库。考虑为您的测试提供此类 DAO 的内存实现 - 一个类,它不是在真实数据库中持久化对象,而是将所有对象存储在内部 ConcurrentMap<K,V> 中。多亏了这一点,您仍然可以将测试作为单元测试运行,并且您可以测试将 createNewKey 参数设置为 true 的 API 密钥对是否完全符合您的预期。或者,您可以编写一个带有 H2 数据库替换的集成测试,但这只会使您的测试引导程序更长。选择权在你。

有一条规则值得记住 - 如果您的类/组件/功能等难以进行单元测试,则意味着做出了错误的设计选择。

替代方案:Spock 的 Spy 对象

我最后故意提到一件事。 Spock 支持所谓的“间谍”对象,其行为类似于真实对象,但它们允许您存根某些部分并将该对象视为模拟对象,例如调用计数。但即使是 Spock 作者也告诫开发人员不要使用此功能:

(在使用此功能之前请三思。根据规范更改代码的设计可能会更好。)

来源:http://spockframework.org/spock/docs/1.0/interaction_based_testing.html#spies

我不知道 Grails 是否有用于创建 Spy 而不是 Mock 的测试的注释,但您始终可以follow official documentation 并创建普通的 Spock 单元测试,将您的服务实例化为 Spy,然后您可以尝试计数调用。不过我不建议这样做,只是为了记录而提及。

【讨论】:

  • 这是一个非常好的答案,我只是不想为另一个甚至不提供完整测试或应用程序类的问题写一个类似的。对于 OP 在他的问题中投入的一点点努力,答案实际上太过分了。无论如何,对 Szymon 大加赞赏。我确实想知道为什么人们如此热衷于通过检查交互来测试每个琐碎类的内部实现,而不是首先尝试进行良好的功能测试。交互只应在必要时进行测试,以检查例如的正确实现。像观察者这样的设计模式。
  • 感谢@kriegaex 的客气话!我认为进行基于交互的测试是学习编写良好测试之旅的里程碑之一。我去过那里,我也做过同样的事情,并且我了解到有更好的方法来测试代码。如果 OP 进行 TDD 循环,他甚至不会考虑测试交互,他会专注于测试结果和极端情况,无论内部实现是什么。如果这些错误是学习过程的副作用,那么犯这些错误是可以接受的。这就是 SO 的意义所在——互相教导和学习。最好的!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-11-05
  • 2020-11-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-06
  • 1970-01-01
相关资源
最近更新 更多