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?
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 String
s, 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 String
s - another empty array of String
s, 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()
}
}