kotlinmicronautmockk

Why can't mockk verify that my Micronaut meter counter got incremented?


I want to test that some code updates Micronaut (4.7.5) metrics. (In real life, the code under test gets passed the MeterRegistry and calls its methods.)

import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Tags
import io.mockk.*
import org.junit.jupiter.api.Test

fun MeterRegistry.countIt(): Counter = counter("my-counter")

fun MeterRegistry.countTagged(tag: String): Counter = counter("other-counter", Tags.of(Tag.of("tag", tag)))

class RegistryTest {
    @Test
    fun `test counter`() {
        val registry: MeterRegistry = mockk(relaxed = true)
        registry.countIt().increment()
        verify {  // fails
            registry.countIt().increment()
        }
    }

    @Test
    fun `test counter with tag`() {
        val registry: MeterRegistry = mockk(relaxed = true)
        registry.countTagged("tag").increment()
        verify {  // passes
            registry.countTagged("tag").increment()
        }
    }
}

The first test always fails with

Verification failed: call 2 of 2: Counter(child of #4#6).increment()) was not called

and the 2nd one always passes.

Why is this and how can I verify that the first metric gets incremented?


Solution

  • The root of the problem is hidden away in the signature of the counter functions in MeterRegistry. The test fails because counter("my-counter") is a call to a var-arg function:

    public Counter counter(String name, String... tags)
    

    This can be verified by replacing MeterRegistry with a Kotlin class

    class MeterRegistry {
        fun counter(name: String, vararg tags: String): Counter = TODO()
        fun counter(name: String, tags: Iterable<Tag>): Counter = TODO()
    }
    

    Using this class, we obtain the same result that the first test fails. When we remove the vararg parameter, the test succeeds.

    So why is it a problem that we have a vararg parameter? Is it a bug? No.

    The verification fails due to the relaxed mock in combination with a chained call in the verify block.

    Let's first look at the expression

    verify {  // passes
        registry.countTagged("tag").increment()
    }
    

    Since countTagged is an extension function that is not defined in MeterRegistry but somewhere else, having MeterRegistry only as a receiver, it is not mocked. Thus, we do not verify that registry.countTagged("tag") was called, but instead we verify that its evaluation registry.counter("other-counter", Tags.of(Tag.of("tag", tag))) was called, where counter is actually a mocked function of MeterRegistry. Then, for a mock m returned by that call, we verify that m.increment() was called.

    Now, let's look at the expression

    verify {  // fails
        registry.countIt().increment()
    }
    

    Again, countIt is not defined in MeterRegistry and therefore is not mocked, but its evaluation counter("my-counter") corresponds to a call of mocked function counter with two arguments: a String "my-counter", and an empty array of Strings, which is the value of zero arguments for vararg parameter tags. Then again, for a mock m returned by that call, we verify that m.increment() was called. Which did not happen.

    But why?

    Because we did not call counter with the same arguments before, and therefore never created the mock m for which we are trying to verify that m.increment() was called. We called counter once before, but with different arguments: a String "my-counter", and an empty array of Strings - another empty array of Strings, because arrays are not equal by content, but only by reference, i.e. if they are the same instance.

    The test can be repaired by explicitly specifying the mocking of counter without any tags:

    @Test
    fun `test counter`() {
        val mockCounter = mockk<Counter>(relaxUnitFun = true)
        val registry: MeterRegistry = mockk {
            every { counter("my-counter") } returns mockCounter
        }
        registry.countIt().increment()
        verify {
            registry.countIt()
            mockCounter.increment()
        }
    }