Mocking Kotlin higher order functions in Spock

184 Views Asked by At

Unable to figure out how to mock higher order functions using spock. Example code snippets:

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

Spock test:

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 * _
  }
}

As stated in the few comments in the snippets, someOtherClass::foo returns differently between the Kotlin code (KFunction) and Groovy/Spock (Groovy Closure). I haven't found anyways to get the actual KFunction for mocking, it should really just be a reference to a function so I feel the mocking shouldn't be all that hard I am just missing something here.

Have tried trying to cast Groovy closure to KFunction with no luck (didn't expect it to work), tried using just the plain SomeOtherClass::foo instead of the specific mock instance but still was a Groovy closure, etc. All paths have lead to :

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
2

There are 2 best solutions below

1
kriegaex On

I am not a Kotlin user. At first, I was wondering why my error message differed from yours:

1 * someClass.bar(function foo (Kotlin reflection is not available))   <-- this triggered the error

The reason is explained here: I had to add kotlin-reflect as a dependency to my sample Kotlin module.

Solution 1: Use Kotlin reflection to determine argument details

Unfortunately, there does not seem to be a good way to use the Kotlin function reference in Groovy. So the best we can do is

  • to verify the type Function0<Unit> and
  • use an argument constraint closure as explained here and match its toString() output as good as we can.

Here is a variant which matches both the variant without and with 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 * _
  }
}

Or, if you prefer substring matching to regex matching:

    1 * someClass.bar(
      {
        it.toString().startsWith('function foo ') ||
          it.toString().endsWith('.SomeOtherClass.foo(): kotlin.Unit')
      } as Function0<Unit>
    )

if you want to relax your argument constraint to just check the type, you can of course simply use:

    1 * someClass.bar(_ as Function0<Unit>)

There might be a better, more precise way to solve this, but someone more versed in both Groovy and Kotlin would have to answer that.

Solution 2: Use Spock spy to pass through method call to secondary mock and verify on the latter being called

Like Leonard already said in his comment, you really seem to be over-specifying your feature in a very rigid way, making the specification brittle with regard to refactoring in your subject under specification.

Anyway, you can use something I would call a hybrid between a mock and a spy, i.e. a test double wrapped around a real object instance, but returning mock responses by default where a normal spy would pass through all method calls. Then, you pass through the one method call you are interested in to the wrapped instance, which in this case is yet another mock that you can verify the method call on.

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 * _
  }
}

Of course, for this simple setup, the default response + override solution is unnecessary, because SomeClass only has one method anyway. But if your real class has multiple methods and you really want to mock it as much as possible, because it is a test dependency rather than the subject under specification, the above is a way to achieve that.

1
Steyrix On

In some cases it can be possible to relax the arguments and call the passed functions to test their behavior rather that type/name equality.

In your case, lambda which passed to bar gets invoked in it.

For example, if the passed function call some mocks you can do something like:

1 * someClass.bar(_) { Function0<Unit> func ->
    func.invoke()
}
1 * someMock.doSomething() // can happen after invocation of lambda