groovy 在Spock中模拟Kotlin高阶函数

64jmpszr  于 7个月前  发布在  Kotlin
关注(0)|答案(2)|浏览(62)

无法弄清楚如何使用spock模拟高阶函数。示例代码片段:

import jakarta.inject.Singleton

@Singleton
class SomeClass {
  fun bar(function: () -> Unit) {
    function()
  }
}

@Singleton
class SomeOtherClass {
  fun foo() {
    println("foo")
  }
}

@Singleton
class ClassUnderTest(
  private val someClass: SomeClass,
  private val someOtherClass: SomeOtherClass,
) {
  fun test() {
    // someOtherClass::foo type is KFunction0<Unit>
    someClass.bar(someOtherClass::foo)
  }
}

史波克测试:

class ClassUnderTestSpecification extends Specification {

  def someClass = Mock(SomeClass)
  def someOtherClass = Mock(SomeOtherClass)
  def classUnderTest = new ClassUnderTest(someClass, someOtherClass)

  def 'test class'() {
    when:
    classUnderTest.test()

    then:
    // someOtherClass::foo type is Groovy Closure
    // fails, as it should be the Kotlin KFunction
    1 * someClass.bar(someOtherClass::foo)
    0 * _
  }
}

正如片段中的一些注解所述,someOtherClass::foo在Kotlin代码(KFunction)和Groovy/Spock(Groovy Closure)之间的返回方式不同。我还没有找到任何方法来获得实际的KFunction进行mocking,它应该只是一个函数的引用,所以我觉得mocking应该不会那么难,我只是在这里缺少了一些东西。
尝试过将Groovy闭包转换为KFunction,但没有成功(没有期望它能工作),尝试过只使用普通的SomeOtherClass::foo而不是特定的mock示例,但仍然是一个Groovy闭包,等等。所有道路都通向:

Too many invocations for:

0 * _   (1 invocation)

Matching invocations (ordered by last occurrence):

1 * someClass.bar(fun com.example.package.SomeOtherClass.foo(): kotlin.Unit)   <-- this triggered the error
xqkwcwgp

xqkwcwgp1#

在某些情况下,可以放松参数并调用传递的函数来测试它们的行为,而不是类型/名称相等。
在您的例子中,传递给bar的lambda在其中被调用。
例如,如果传递的函数调用了一些mock,你可以这样做:

rwqw0loc

rwqw0loc2#

我不是Kotlin用户。首先,我想知道为什么我的错误消息与你的不同:
原因解释为here:我必须将kotlin-reflect作为依赖项添加到示例Kotlin模块中。

方案一:使用Kotlin反射来确定参数细节

不幸的是,在Groovy中似乎没有使用Kotlin函数引用的好方法。所以我们能做的就是

  • 验证Function0<Unit>类型,
  • 使用这里解释的参数约束闭包,并尽可能好地匹配toString()输出。

下面是一个与不带kotlin-reflect和带kotlin-reflect的变体相匹配的变体:

import kotlin.Unit
import kotlin.jvm.functions.Function0
import spock.lang.Specification

class KotlinHigherOrderFunctionTest extends Specification {
  def someClass = Mock(SomeClass)
  def someOtherClass = Mock(SomeOtherClass)
  def classUnderTest = new ClassUnderTest(someClass, someOtherClass)

  def 'test class'() {
    when:
    classUnderTest.test()

    then:
    1 * someClass.bar(
      { it =~ /function foo |fun .*\.SomeOtherClass\.foo\(\): kotlin\.Unit/ } as Function0<Unit>
    )
    0 * _
  }
}

或者,如果你更喜欢子串匹配而不是正则表达式匹配:
如果你想放松你的参数约束,只检查类型,你当然可以简单地使用:用途:
可能有更好、更精确的方法来解决这个问题,但是必须有更精通Groovy和Kotlin的人来回答这个问题。

方案二:使用Spock spy将方法调用传递给二级模拟,并在调用后者时进行验证

就像伦纳德已经在他的评论中说过的,你似乎真的以一种非常严格的方式过度指定了你的特性,使得规范在重构规范下的主题时变得脆弱。
无论如何,你可以使用一些我称之为模拟和间谍之间的混合体的东西,即。一个测试double包裹在一个真实的对象示例周围,但默认情况下返回模拟响应,而普通的spy将通过所有方法调用。然后,将您感兴趣的一个方法调用传递给 Package 的示例,在本例中, Package 的示例是另一个mock,您可以在其上验证方法调用。

import org.spockframework.mock.ZeroOrNullResponse
import spock.lang.Specification

class KotlinHigherOrderFunctionTest extends Specification {
  def 'test class'() {
    given:
    // Spy behaving like a quasi mock, i.e. not passing through calls
    // to real methods by default, but returning mock responses
    SomeClass someClass = Spy(defaultResponse: ZeroOrNullResponse.INSTANCE)
    SomeOtherClass someOtherClass = Mock()
    def classUnderTest = new ClassUnderTest(someClass, someOtherClass)

    when:
    classUnderTest.test()

    then:
    // For SomeClass.bar, pass through method calls to the original method  
    1 * someClass.bar(_) >> { args -> callRealMethod() }
    // Expect the original method call to be SomeOtherClass.foo 
    1 * someOtherClass.foo()
    0 * _
  }
}

当然,对于这个简单的设置,默认的响应+覆盖解决方案是不必要的,因为SomeClass只有一个方法。但是如果你的真实的类有多个方法,并且你真的想尽可能多地模拟它,因为它是一个测试依赖项而不是规范下的主题,上面是一种实现方法。

相关问题