Mockk: Feature: inline classes

Created on 6 Oct 2018  路  21Comments  路  Source: mockk/mockk

I am trying to use Kotlin's experimental inline class feature. I have an interface with a method accepting a parameter with an inline class type. In this case, every function throws an exception.

Prerequisites

My Kotlin version is: 1.3.0-rc-146

  • [x] I am running the latest version - 1.8.9.kotlin13
  • [x] I checked the documentation and found no answer
  • [x] I checked to make sure that this issue has not already been filed

Expected Behavior

I should be able to define every with any() in parameters.

Current Behavior

Test throws an exception.

Failure Information (for bugs)

See the exception details in Failure logs section.

Steps to Reproduce

This is a test case. I am running this with Kotlin 1.3.0-rc-146, kotlin-reflect 1.3.0-rc-146, JUnit 5.1.0, mockk 1.8.9.kotlin13.

import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test

class MockkFailingTest {
    @Test // fails
    fun `mockk test using any()`() {
        val mocked = mockk<TestMock>(relaxed = true)
        every { mocked.test(any()) } returns 1
    }

    @Test // passes
    fun `mockk test using hardcoded value`() {
        val mocked = mockk<TestMock>(relaxed = true)
        every { mocked.test(MyInlineType("123")) } returns 1
    }

}

inline class MyInlineType(val value: String)

interface TestMock {
    fun test(value: MyInlineType): Int
}

Context

Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions.

  • MockK version: 1.8.9.kotlin13
  • OS: Ubuntu 18.04
  • Kotlin version: 1.3.0-rc-146
  • JDK version: 1.8
  • JUnit version: 5.1.0
  • Type of test: unit test

Failure Logs

io.mockk.MockKException: Failed matching mocking signature for
SignedCall(retValue=0, isRetValueMock=false, retType=class kotlin.Int, self=TestMock(#2), method=test-gNmuAAA(String), args=[null], invocationStr=TestMock(#2).test-gNmuAAA(null))
left matchers: [any()]

    at io.mockk.impl.recording.SignatureMatcherDetector.detect(SignatureMatcherDetector.kt:99)
    at io.mockk.impl.recording.states.RecordingState.signMatchers(RecordingState.kt:38)
    at io.mockk.impl.recording.states.RecordingState.round(RecordingState.kt:30)
    at io.mockk.impl.recording.CommonCallRecorder.round(CommonCallRecorder.kt:45)
    at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:47)
    at io.mockk.impl.eval.EveryBlockEvaluator.every(EveryBlockEvaluator.kt:25)
    at io.mockk.MockKDsl.internalEvery(API.kt:93)
    at io.mockk.MockKKt.every(MockK.kt:104)
    at conduit.handler.LoginHandlerImplTest.mockk test(LoginHandlerImplTest.kt:79)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:436)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:170)
    at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:166)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:113)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:58)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:112)
    at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:120)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
    at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
    at java.util.Iterator.forEachRemaining(Iterator.java:116)
    at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:120)
    at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:120)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
    at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
    at java.util.Iterator.forEachRemaining(Iterator.java:116)
    at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:120)
    at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:55)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:43)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:170)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:154)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:90)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:74)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
enhancement important

Most helpful comment

finally I got it working even for return values see code below

Usage Example for inline class kotlin.time.Duration

mockk<Dummy> {
    every { functionWithDurationParameter(anyValue()) } returns value(3.days)
}

Mockk Extension Functions

import io.mockk.ConstantMatcher
import io.mockk.MockKGateway.CallRecorder
import io.mockk.MockKMatcherScope
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor

fun <T : Any> value(value: T): T =
    if (value::class.isInline) inlineValue(value)
    else value

@Suppress("UNCHECKED_CAST")
fun <T : Any> inlineValue(value: T): T {
    val valueName = value::class.primaryConstructor!!.parameters[0].name
    val valueProperty = value::class.declaredMemberProperties
        .find { it.name == valueName }!! as KProperty1<T, *>
    return valueProperty.get(value) as T
}

inline fun <reified T : Any> MockKMatcherScope.anyValue(): T =
    if (T::class.isInline) anyInlineValue()
    else any()


inline fun <reified T : Any> MockKMatcherScope.anyInlineValue(): T {
    val valueConstructor = T::class.primaryConstructor!!
    val valueType = valueConstructor.parameters[0].type.classifier as KClass<*>
    val callRecorder = getProperty("callRecorder") as CallRecorder
    val anyMatcher = callRecorder.matcher(ConstantMatcher<T>(true), valueType)
    return valueConstructor.call(anyMatcher)
}

val KClass<*>.isInline: Boolean
    get() = !isData &&
        primaryConstructor?.parameters?.size == 1 &&
        java.declaredMethods.any { it.name == "box-impl" }

All 21 comments

Thanks for reporting it.

verify throws same exception

And cannot capture an inline class using slot either.

To everyone that might come back to this issue, I found the way to make this work.

Before writing down the solution, I want to make clear the following points:

  • mock checks are performed using the .equals() method, which is the same thing as == in Kotlin
  • inline classes are only wrappers around a specific other class (in this case, MyInlineType is a wrapper around String)
  • inline classes' .equals() method just calls the wrapped value's .equals() method

With this in mind, we can re-write the following tests:

@Test // fails
fun `mockk test using any()`() {
    val mocked = mockk<TestMock>(relaxed = true)
    every { mocked.test(any()) } returns 1
}

As the following

@Test // works
fun `mockk test using any()`() {
    val mocked = mockk<TestMock>(relaxed = true)
    every { mocked.test(MyInlineType(any())) } returns 1
}

This will work, as the MyInlineType will be un-wrapped and the internal value will be checked without a problem later on.

I hope this is useful to everyone.

@oleksiyp Probably this might be wrote down into the README after more tests have been made.

@RiccardoM that looks like a good workaround for the matching use case. What about the _"method returns an inline class"_ use case?

For example:

inline class TestMock(val value: String)

interface MyInterface {
  fun returnsMock(): TestMock
}

And tests:

internal class MockkInlineTest {

  @Test
  internal fun `returns example`() {
    val myInterface = mockk<MyInterface>()

    every { myInterface.returnsMock() } returns TestMock("hello")

    assertEquals(
      TestMock("hello"),
      myInterface.returnsMock()
    )
  }

  @Test
  internal fun `answers lambda example`() {
    val myInterface = mockk<MyInterface>()

    every { myInterface.returnsMock() } answers { TestMock("hello") }

    assertEquals(
      TestMock("hello"),
      myInterface.returnsMock()
    )
  }

  @Test
  internal fun `answers implementation example`() {
    val myInterface = mockk<MyInterface>()

    val answer = object : Answer<TestMock> {
      override fun answer(call: Call): TestMock {
        return TestMock("hello")
      }
    }
    every { myInterface.returnsMock() } answers(answer)

    assertEquals(
      TestMock("hello"),
      myInterface.returnsMock()
    )
  }
}

On Java 11 and Kotlin 1.3.31, all of these fail with a similar variant to:

java.lang.ClassCastException: class TestMock cannot be cast to class java.lang.String (TestMock is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')

    at MyInterface$Subclass0.returnsMock(Unknown Source)
    at MockkInlineTest.answers implementation example

In Mockito, answer seemed to work as a workaround (that I found in https://github.com/nhaarman/mockito-kotlin/issues/309), but it doesn't seem to be working here. Any suggestions would be helpful.

What I have been unfortunately doing is creating fakes instead of utilizing Mockk in these places

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. If you are sure that this issue is important and should not be marked as stale just ask to put an important label.

I don't believe this should be closed (either is or other issue) - can somebody add the important label?

I use klock library which has an inline class at its core by design. I wanted to use mockk, but the lack of this feature/fix is a blocker to me to write unit tests.

If mockk doesn't want to miss new clients and lose existing ones, I suggest providing a complete workaround or even ugly, experimental API for this, and introduce proper API/fix in implementation later. Otherwise I and probably a bunch of other people will have to look for another library.

I understand that inline classes are an experimental feature of Kotlin and clients of libraries can expect undefined behavior in such case, but looks like authors of some libraries decide to rely on the experimental features really deeply and then it sort of forces other libraries to treat it in a "less experimental" way.

@edit: I found a workaround for returning instances of inline classes, which is satisfactory for me for now. Instead of not working (DateTime is an inline class):

val timeProvider = mockk<TimeProvider>()
every { timeProvider.now() } returns DateTime.fromUnix(123)

if you don't care about verifying the mock's call, use just a regular object, or if you do care, wrap it in a spy:

val timeProvider = spyk<TimeProvider>(object : TimeProvider {
    override fun now() = DateTime.fromUnix(123)
})

Thanks @krzema12 - your workaround worked for me and saved my day

unfortunately it made the code quite ugly - so I would really like to see this fixed.

I have the same problem, but all the workarounds here are focusing on returning inline classes. But I need to verify the call of method with inline class as it's parameter.

inline class Group(val uuid: UUID) {
    constructor(uuid: String) : this(UUID.fromString(uuid))
}

And my test looks like this:

private companion object {
    private val GROUP_UUID = Group("4566900c-91b2-43cc-953e-28fe6354bf57")
}

@Test
fun testSomething() = runBlocking {
    doSomething()
    coVerify(inverse = true) { foo.bar(any()) }
}

And this fails with:

io.mockk.MockKException: Failed matching mocking signature for
SignedCall(..., method=bar-x2tluNw(UUID, Continuation), args=[null, Continuation at ...], invocationStr=Foo(foo#61).bar-x2tluNw(null, continuation {}))
left matchers: [any()]

The solution for verify is to rewrite:

  • any() to Group(any<UUID>()).
  • eq(GROUP_UUID) to Group(eq(GROUP_UUID.uuid))
  • etc.

Proposed workaround doesn't work with kotlin.time.Duration because Duration ctor is internal and

 every { mocked.test(Duration(any())) } returns 1

impossible.

I've tried

every { mocked.test(any<Double>().seconds) } returns 1

but no luck.

Any ideas?

I'm also having troubles with kotlin.time.Duration because of its internal constructor. Any fixes or workarounds?

I found a workaround for inline classes with inaccessible constructors like kotlin.time.Duration: make the constructor accessible 馃槷

Define a function like this:

fun MockKMatcherScope.anyDuration(): Duration {
    val ctor = Duration::class.constructors.first()
    ctor.isAccessible = true

    return ctor.call(any<Double>())
}

Usage:

every { mocked.test(anyDuration()) } returns 1

@blazeroni A slightly more generic Workaround

inline fun <reified T : Any, reified I : Any> MockKMatcherScope.anyInline() =
    T::class.constructors.first()
        .apply { isAccessible = true }
        .call(any<I>())

@blazeroni a even more generic version, without the need to declare value type

inline fun <reified T : Any> MockKMatcherScope.anyInline(): T {
    val constructor = T::class.primaryConstructor!!
    val valueType = constructor.parameters[0].type.classifier as KClass<*>
    val any = (getProperty("callRecorder") as MockKGateway.CallRecorder)
        .matcher(ConstantMatcher<T>(true), valueType)
    return constructor.call(any)
}

@blazeroni and finally a total replacement

inline fun <reified T : Any> MockKMatcherScope.anyValue(): T =
    if (T::class.isInline) anyInline()
    else any()

inline fun <reified T : Any> MockKMatcherScope.anyInline(): T =
    T::class.primaryConstructor!!.run {
        val valueType = parameters[0].type.classifier as KClass<*>
        call(match(ConstantMatcher<Any>(true), valueType))
    }

fun <T : Any> MockKMatcherScope.match(matcher: Matcher<T>, type: KClass<T>): T =
    (getProperty("callRecorder") as MockKGateway.CallRecorder).matcher(matcher, type)

val KClass<*>.isInline: Boolean
    get() = !isData &&
        primaryConstructor?.parameters?.size == 1 &&
        java.declaredMethods.any { it.name == "box-impl" }

however inline class as return values still not working :-/
... returns 3.days gives following exception at runtime
java.lang.ClassCastException: class kotlin.time.Duration cannot be cast to class java.lang.Double (kotlin.time.Duration is in unnamed module of loader 'app'; java.lang.Double is in module java.base of loader 'bootstrap')

finally I got it working even for return values see code below

Usage Example for inline class kotlin.time.Duration

mockk<Dummy> {
    every { functionWithDurationParameter(anyValue()) } returns value(3.days)
}

Mockk Extension Functions

import io.mockk.ConstantMatcher
import io.mockk.MockKGateway.CallRecorder
import io.mockk.MockKMatcherScope
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor

fun <T : Any> value(value: T): T =
    if (value::class.isInline) inlineValue(value)
    else value

@Suppress("UNCHECKED_CAST")
fun <T : Any> inlineValue(value: T): T {
    val valueName = value::class.primaryConstructor!!.parameters[0].name
    val valueProperty = value::class.declaredMemberProperties
        .find { it.name == valueName }!! as KProperty1<T, *>
    return valueProperty.get(value) as T
}

inline fun <reified T : Any> MockKMatcherScope.anyValue(): T =
    if (T::class.isInline) anyInlineValue()
    else any()


inline fun <reified T : Any> MockKMatcherScope.anyInlineValue(): T {
    val valueConstructor = T::class.primaryConstructor!!
    val valueType = valueConstructor.parameters[0].type.classifier as KClass<*>
    val callRecorder = getProperty("callRecorder") as CallRecorder
    val anyMatcher = callRecorder.matcher(ConstantMatcher<T>(true), valueType)
    return valueConstructor.call(anyMatcher)
}

val KClass<*>.isInline: Boolean
    get() = !isData &&
        primaryConstructor?.parameters?.size == 1 &&
        java.declaredMethods.any { it.name == "box-impl" }

@oleksiyp any chance we can get @qoomon's extension functions above shipped with mockk? Especially the inlineValue function is necessary any time you want to mock a field on a class that is modelled as an inline class.

Related: Mocking calls on inline class instances fails, unless you use a relaxed mock:

inline class InlinedInt(val value: Int)

internal class TheTest {
    @Test
    fun inlinedFailsIfNotRelaxed() {
        val mockedInlinedInt = mockk<InlinedInt>()
        // --> no answer found for: InlinedInt(#1).unbox-impl()
    }

    @Test
    fun inlinedSucceedsIfRelaxed() {
        val mockedInlinedInt = mockk<InlinedInt>(relaxed = true)
    }
}

We can't use every/coEvery with mach to match parameters whose value is an inlined class. Instead, use slot and capture:

import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.slot
import org.junit.jupiter.api.Test
import org.assertj.core.api.Assertions.assertThat

inline class EatableUInt(val x: Int)

interface UIntEater {
    fun eat(input: EatableUInt)
}

class InlineClassParameterMockkTest {
    val eater: UIntEater = mockk()

    @Test
    fun test() {
        val wrappedValueSlot = slot<Int>()
        every {
            // As of Mockk 1.10.0 we can't use match {...} for parameters
            // whose types are inline classes. Hence capture/slot.
            eater.eat(EatableUInt(capture(wrappedValueSlot)))
        } just Runs

        eater.eat(EatableUInt(0))
        assertThat(wrappedValueSlot.captured).isEqualTo(0)
    }
}
Was this page helpful?
0 / 5 - 0 ratings