2

In the MWE below I'm trying to verify that calling baz() also calls a method on another object. However, I can't seem to mock / spy on that object.

MWE:

package com.example

import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.verify
import org.junit.jupiter.api.Test

class FooBarTest {
    @Test
    fun `top level fun baz() calls theVal_bar()`() {
        mockkStatic("com.example.FooBarTestKt")
        val spy = spyk(theVal, name = "Hello, Spy!")

        every { theVal } returns spy

        // Should call bar() on the spy, but note that the spy's name is not printed
        baz()

        verify { spy.bar() }
    }
}

class Foo

fun Foo.bar() = println("Foo.bar! name = $this")

val theVal = Foo()

fun baz() = theVal.bar()

This fails, because the call to theVal.bar() gets the val initialiser value instead of the mocked value spy.

How can I enforce the spy being used without changing the top level property definitions? In other words: I need a top level 'constant', but I want to mock it too. I could use val theVal get() = Foo(), which solves the issue, but it changes the code significantly, as it would replace the Foo instance every time.

Versions used: - Kotlin 1.3.10 - MockK 1.8.13.kotlin13 - JUnit 5.3.1

The error:

java.lang.AssertionError: Verification failed: call 1 of 1: class com.example.FooBarTestKt.bar(eq(Foo(Hello, Spy!#1)))). Only one matching call to FooBarTestKt(static FooBarTestKt)/bar(Foo) happened, but arguments are not matching:
[0]: argument: com.example.Foo@476b0ae6, matcher: eq(Foo(Hello, Spy!#1)), result: -
Erik
  • 4,305
  • 3
  • 36
  • 54

2 Answers2

3

Oh it is really madness when it comes to static and object mockks, and extension functions. To survive just think of extension functions as static functions with an argument.

Check, this is working because fooInstance is just an object passed as the first argument:

    mockkStatic("kot.TestFileKt")

    baz()

    val fooInstance = theVal

    verify { fooInstance.bar() }

Combining it doesn't work:

    verify { theVal.bar() }

because it is as well verified.

This will also work(as I said Foo is just first argument to static method) as well:

    mockkStatic("kot.TestFileKt")

    baz()

    verify { any<Foo>().bar() }
oleksiyp
  • 2,659
  • 19
  • 15
0

Instead of using an initialiser, use a backing (private) property and use get() for the val to be mocked:

private val _theVal = Foo()
val theVal get() = _theVal

Using a getter instead of initialiser creates a getter method without a static backing field. You can check the bytecode to see this:

Kotlin:

package com.example

@JvmField // See also: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#instance-fields
val thisIsAField = "I'm static!"

val thisIsAValWithInitialiser = "I'm a static field too!"

val thisIsAValWithGetter get() = "I'm hardcoded in the getter method!"

Bytecode (I've removed much of the clutter so that my point becomes easier to see):

public final static Ljava/lang/String; thisIsAField

private final static Ljava/lang/String; thisIsAValWithInitialiser

public final static getThisIsAValWithInitialiser()Ljava/lang/String;
L0
LINENUMBER 6 L0
GETSTATIC com/example/FooBarTestKt.thisIsAValWithInitialiser : Ljava/lang/String;
ARETURN
L1

public final static getThisIsAValWithGetter()Ljava/lang/String;
L0
LINENUMBER 8 L0
LDC "I'm hardcoded in the getter method!"
ARETURN
L1

static <clinit>()V
L0
LINENUMBER 4 L0
LDC "I'm static!"
PUTSTATIC com/example/FooBarTestKt.thisIsAField : Ljava/lang/String;
L1
LINENUMBER 6 L1
LDC "I'm a static field too!"
PUTSTATIC com/example/FooBarTestKt.thisIsAValWithInitialiser : Ljava/lang/String;
RETURN

What can you see here? There is an important similarity between thisIsAField and thisIsAValWithInitialiser, being that they are backed by static fields. The getter method of thisIsAValWithInitialiser just returns that value. The value is private.

The similarity between thisIsAValWithInitialiser and thisIsAValWithGetter is that they are both public getter methods, but the difference is that the return value of thisIsAValWithGetter is hardcoded in the method body. This is simply a public method that MockK can override (even though it is final).

I guess (as I don't know the internals) that MockK cannot overrule GETSTATIC com/example/FooBarTestKt.thisIsAValWithInitialiser : Ljava/lang/String;, which is why a val initialiser cannot be mocked.

Erik
  • 4,305
  • 3
  • 36
  • 54