Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: inline classes #152

Closed
3 tasks done
alisabzevari opened this issue Oct 6, 2018 · 58 comments · Fixed by #849
Closed
3 tasks done

Feature: inline classes #152

alisabzevari opened this issue Oct 6, 2018 · 58 comments · Fixed by #849

Comments

@alisabzevari
Copy link

alisabzevari commented Oct 6, 2018

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

  • I am running the latest version - 1.8.9.kotlin13
  • I checked the documentation and found no answer
  • 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)
@oleksiyp
Copy link
Collaborator

oleksiyp commented Oct 6, 2018

Thanks for reporting it.

@oleksiyp oleksiyp added the bug label Nov 3, 2018
@manusobles
Copy link

verify throws same exception

@manusobles
Copy link

And cannot capture an inline class using slot either.

@oleksiyp oleksiyp changed the title any() doesn't work for inline classes Feature: inline classes Dec 25, 2018
@oleksiyp oleksiyp added enhancement and removed bug labels Dec 25, 2018
@oleksiyp oleksiyp added this to To do in MockK features Dec 25, 2018
@RiccardoM
Copy link

RiccardoM commented Jan 23, 2019

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.

@mkobit
Copy link
Contributor

mkobit commented May 29, 2019

@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 mockito/mockito-kotlin#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

@stale
Copy link

stale bot commented Jul 28, 2019

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.

@stale stale bot added the stale label Jul 28, 2019
@mkobit
Copy link
Contributor

mkobit commented Jul 28, 2019

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

@stale stale bot removed the stale label Jul 28, 2019
@krzema12
Copy link

krzema12 commented Sep 27, 2019

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

@ligi
Copy link

ligi commented Dec 5, 2019

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.

@Pitel
Copy link

Pitel commented Dec 9, 2019

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.

@underlow
Copy link

underlow commented Feb 7, 2020

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?

@Zhelyazko
Copy link

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

@blazeroni
Copy link

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

@qoomon
Copy link
Contributor

qoomon commented May 20, 2020

@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>())

@qoomon
Copy link
Contributor

qoomon commented May 20, 2020

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

@qoomon
Copy link
Contributor

qoomon commented May 20, 2020

@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" }

@qoomon
Copy link
Contributor

qoomon commented May 21, 2020

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

@qoomon
Copy link
Contributor

qoomon commented May 22, 2020

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

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


@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.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" }

@qoomon
Copy link
Contributor

qoomon commented May 28, 2021

Should work now.

@TheBestPessimist
Copy link

TheBestPessimist commented Jul 1, 2021

on mockK 1.12 this still doesn't work (after #633):

Please see the following example:

import io.mockk.every
import io.mockk.junit5.MockKExtension
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(MockKExtension::class)
internal class a {
    @Test
    fun t() {
        val aService = mockk<AService>()

        every { aService.getById(any()) } returns AServiceIdReturn("tralala") // this fails with NPE

        assertThat(aService.getById(AServiceId("1"))).usingRecursiveComparison()
            .isNotEqualTo(AServiceIdReturn("123"))
            .isEqualTo(AServiceIdReturn("tralala"))
    }
}

class AService {
    fun getById(id: AServiceId): AServiceIdReturn {
        return AServiceIdReturn("123")
    }
}

@JvmInline
value class AServiceId(val s: String)

@JvmInline
value class AServiceIdReturn(val s: String)

And the stack trace is:

14:04:03.871 [Test worker] DEBUG io.mockk.impl.instantiation.AbstractMockFactory - Creating mockk for AService name=#1

java.lang.NullPointerException
	at io.mockk.impl.recording.states.RecordingState.matcher(RecordingState.kt:53)
	at io.mockk.impl.recording.CommonCallRecorder.matcher(CommonCallRecorder.kt:52)
	at com.bla.a$t$1.invoke-Cv2eMwI(a.kt:37)
	at com.bla.a$t$1.invoke(a.kt:16)
	at io.mockk.impl.eval.RecordedBlockEvaluator$record$block$1.invoke(RecordedBlockEvaluator.kt:25)
	at io.mockk.impl.eval.RecordedBlockEvaluator$enhanceWithRethrow$1.invoke(RecordedBlockEvaluator.kt:78)
	at io.mockk.impl.recording.JvmAutoHinter.autoHint(JvmAutoHinter.kt:23)
	at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:40)
	at io.mockk.impl.eval.EveryBlockEvaluator.every(EveryBlockEvaluator.kt:30)
	at io.mockk.MockKDsl.internalEvery(API.kt:92)
	at io.mockk.MockKKt.every(MockK.kt:98)

@qoomon
Copy link
Contributor

qoomon commented Jul 1, 2021

there is a test in place for any() that works, however it is build with kotlin <1.5 and therefore with inline class instead of value class see https://github.com/mockk/mockk/blob/master/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt.

the problem ist that the initialization for an value class creates an value class object with a null value field. io.mockk.proxy.jvm.ObjenesisInstantiator:42

@boiler23
Copy link

In my case, this works on 1.10.6, and doesn't work with 1.12.0 (kotlin 1.5.21):

@JvmInline
value class SomeData(val d: Long)

interface Worker {
   suspend fun doWork(someData: SomeData): SomeData
}

class UseCase {
    var data = SomeData(0L)
    val worker = ...
    suspend fun work() {
        data = worker.doWork(data)
    }
}

// in test:
val someData = SomeData(0L)
val useCase = UseCase(...)
coEvery { worker.doWork(someData) } returns someData
useCase.work()

I get ClassCastException between SomeData and Long.

@dybarsky
Copy link

dybarsky commented Nov 2, 2021

Hi! I have same ClassCastException. I am using mockk 1.12.0 and kotlin 1.5.31. Any workaround?

@martinformi
Copy link

The same here, my workaround is to convert value classes to just classes - this is performance hit because primitives and inline classes are much better handled, but no fix in sight I am afraid.

@dybarsky
Copy link

Any updates regarding this @Raibaz ?

@aSemy
Copy link
Contributor

aSemy commented Jul 19, 2022

I'm on Kotlin 1.7.0 and value classes aren't supported by mockk

I updated the workaround from this comment to use the isValue flag, and to include the workaround for private primary constructors shared here.

The workarounds are 2 years old though - why aren't they in the main codebase? Have developers moved onto another mocking library?

import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible


// Work-around for mocking inline-classes
// https://github.com/mockk/mockk/issues/152#issuecomment-631796323


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


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


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

I would really appreciate it if there was a similar workaround for slots and capture! I tried creating one based on this comment - but it's very confusing.

// doesn't work :(

inline fun <reified T : Any> MockKMatcherScope.captureValue(slot: CapturingSlot<T>): T =
    if (T::class.isValue) {
        captureInline(slot)
    } else {
        capture(slot)
    }

inline fun <reified T : Any> MockKMatcherScope.captureInline(slot: CapturingSlot<T>): T =
    T::class.primaryConstructor!!.run {
        this.isAccessible = true
        val valueType = parameters[0].type.classifier as KClass<*>
        val valueMatcher = CapturingSlotMatcher(CapturingSlot(), valueType)
        val valueSlot = valueMatcher.captureSlot

        call(match(CapturingSlotMatcher(slot, T::class), valueType)
    }

@qoomon
Copy link
Contributor

qoomon commented Jul 19, 2022

@aSemy I managed to implement a working captureValue function. See full example below.

inline fun <reified T : Any> MockKMatcherScope.anyValue(): T {
    if (!T::class.isValue) return any()

    val constructor = T::class.primaryConstructor!!.apply { isAccessible = true }
    val rawType = constructor.parameters[0].type.classifier as KClass<*>

    val anyRawValue = callRecorder.matcher(ConstantMatcher<T>(true), rawType)
    return constructor.call(anyRawValue)
}

inline fun <reified T : Any> MockKMatcherScope.captureValue(slot: CapturingSlot<T>): T {
    if (!T::class.isValue) return capture(slot)

    val constructor = T::class.primaryConstructor!!.apply { isAccessible = true }
    val rawType = constructor.parameters[0].type.classifier as KClass<*>

    val anyRawValue = callRecorder.matcher(CapturingValueSlotMatcher(slot, constructor, rawType), rawType)
    return constructor.call(anyRawValue)
}

val MockKMatcherScope.callRecorder: MockKGateway.CallRecorder
    get() = getProperty("callRecorder") as MockKGateway.CallRecorder

data class CapturingValueSlotMatcher<T : Any>(
    val captureSlot: CapturingSlot<T>,
    val valueConstructor: KFunction<T>,
    override val argumentType: KClass<*>,
) : Matcher<T>, CapturingMatcher, TypedMatcher, EquivalentMatcher {
    override fun equivalent(): Matcher<Any> = ConstantMatcher(true)

    @Suppress("UNCHECKED_CAST")
    override fun capture(arg: Any?) {
        if (arg == null) {
            captureSlot.isNull = true
        } else {
            captureSlot.isNull = false
            captureSlot.captured = valueConstructor.call(arg)
        }
        captureSlot.isCaptured = true
    }

    override fun match(arg: T?): Boolean = true

    override fun toString(): String = "slotCapture<${argumentType.simpleName}>()"
}

@aSemy
Copy link
Contributor

aSemy commented Jul 20, 2022

Great! That looks really tidy. Can we get this committed to the main repo? If you'd like I can take your code and start a PR with it @qoomon?

@qoomon
Copy link
Contributor

qoomon commented Jul 20, 2022

I'm about to implement it and create a PR.

@qoomon
Copy link
Contributor

qoomon commented Jul 20, 2022

@aSemy I think I need help. I can't get the project working. So feel free to create a PR. BTW here are some tests already.

package me.qoomon.enhancements.mockk

import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import org.junit.jupiter.api.Test
import strikt.api.expectThat
import strikt.assertions.isEqualTo

class MockkTest {

    @Test
    fun `any matcher for value class`() {
        // given
        val mock = mockk<ValueServiceDummy>(relaxed = true)
        val givenResult = 1
        every { mock.doSomething(anyValue()) } returns givenResult

        // when
        val result = mock.doSomething(ValueDummy("moin"))

        // then
        expectThat(result).isEqualTo(givenResult)
    }

    @Test
    fun `slot for value class`() {
        // given
        val mock = mockk<ValueServiceDummy>(relaxed = true)
        val slot = slot<ValueDummy>()
        val givenResult = 1
        every { mock.doSomething(captureValue(slot)) } returns givenResult

        val givenParameter = ValueDummy("s")

        // when
        val result = mock.doSomething(givenParameter)

        // then
        expectThat(result).isEqualTo(givenResult)
        expectThat(slot.captured).isEqualTo(givenParameter)
    }

    @Test
    fun `value class as return value`() {
        // given
        val mock = mockk<ValueServiceDummy>(relaxed = true)
        val givenResult = ValueDummy("moin")
        every { mock.getSomething() } returns givenResult

        // when
        val result = mock.getSomething()

        // then
        expectThat(result).isEqualTo(givenResult)
    }
}

@JvmInline
value class ValueDummy(val value: String)

interface ValueServiceDummy {
    fun doSomething(value: ValueDummy): Int
    fun getSomething(): ValueDummy
}

aSemy added a commit to aSemy/mockk that referenced this issue Jul 20, 2022
@qoomon
Copy link
Contributor

qoomon commented Jul 20, 2022

@aSemy

/**
 * POLYFILL for kotlin version < 1.5
 * will be shadowed by implementation in kotlin SDK 1.5+
 *
 * @return true if this is an inline class, else false
 */
private val <T : Any> KClass<T>.isValue: Boolean
    get() = !isData &&
            primaryConstructor?.parameters?.size == 1 &&
            java.declaredMethods.any { it.name == "box-impl" }

Raibaz added a commit that referenced this issue Jul 26, 2022
@dybarsky
Copy link

Thank you <3

@equeim
Copy link

equeim commented Jul 31, 2022

Doesn't seem to work in my case with coEvery :(

Failed matching mocking signature for
SignedCall(retValue=, isRetValueMock=true, retType=class kotlin.collections.List, self=DonkiRepositoryInternal(#52), method=updateEventsForWeek-_A-dL5c(LocalDate, EventType, Continuation), args=[0000-00-00, null, Continuation at org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest$Some of initial load weeks require refresh$3.invokeSuspend(EventsSummariesRemoteMediatorTest.kt:89)], invocationStr=DonkiRepositoryInternal(#52).updateEventsForWeek-_A-dL5c(0000-00-00, null, continuation {}))
left matchers: [any()]
io.mockk.MockKException: Failed matching mocking signature for
SignedCall(retValue=, isRetValueMock=true, retType=class kotlin.collections.List, self=DonkiRepositoryInternal(#52), method=updateEventsForWeek-_A-dL5c(LocalDate, EventType, Continuation), args=[0000-00-00, null, Continuation at org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest$Some of initial load weeks require refresh$3.invokeSuspend(EventsSummariesRemoteMediatorTest.kt:89)], invocationStr=DonkiRepositoryInternal(#52).updateEventsForWeek-_A-dL5c(0000-00-00, null, continuation {}))
left matchers: [any()]
	at app//io.mockk.impl.recording.SignatureMatcherDetector.detect(SignatureMatcherDetector.kt:99)
	at app//io.mockk.impl.recording.states.RecordingState.signMatchers(RecordingState.kt:39)
	at app//io.mockk.impl.recording.states.RecordingState.round(RecordingState.kt:31)
	at app//io.mockk.impl.recording.CommonCallRecorder.round(CommonCallRecorder.kt:50)
	at app//io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:63)
	at app//io.mockk.impl.eval.EveryBlockEvaluator.every(EveryBlockEvaluator.kt:30)
	at app//io.mockk.MockKDsl.internalCoEvery(API.kt:99)
	at app//io.mockk.MockKKt.coEvery(MockK.kt:116)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest.Some of initial load weeks require refresh(EventsSummariesRemoteMediatorTest.kt:89)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest.access$Some of initial load weeks require refresh(EventsSummariesRemoteMediatorTest.kt:26)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest$Some of initial load weeks require refresh ## validate initialize()$1.invokeSuspend(EventsSummariesRemoteMediatorTest.kt:95)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest$Some of initial load weeks require refresh ## validate initialize()$1.invoke(EventsSummariesRemoteMediatorTest.kt)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest$Some of initial load weeks require refresh ## validate initialize()$1.invoke(EventsSummariesRemoteMediatorTest.kt)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest$base$1.invokeSuspend(EventsSummariesRemoteMediatorTest.kt:247)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest$base$1.invoke(EventsSummariesRemoteMediatorTest.kt)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest$base$1.invoke(EventsSummariesRemoteMediatorTest.kt)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$2.invokeSuspend(TestBuilders.kt:212)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$2.invoke(TestBuilders.kt)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$2.invoke(TestBuilders.kt)
	at app//kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:55)
	at app//kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:112)
	at app//kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:126)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:211)
	at app//kotlinx.coroutines.test.TestBuildersKt.runTestCoroutine(Unknown Source)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invokeSuspend(TestBuilders.kt:167)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
	at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:13)
	at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at app//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at app//kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:284)
	at app//kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
	at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
	at app//kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
	at app//kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at app//kotlinx.coroutines.test.TestBuildersJvmKt.createTestResult(TestBuildersJvm.kt:12)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:166)
	at app//kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:154)
	at app//kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest$default(TestBuilders.kt:147)
	at app//kotlinx.coroutines.test.TestBuildersKt.runTest$default(Unknown Source)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest.base(EventsSummariesRemoteMediatorTest.kt:244)
	at app//org.equeim.spacer.donki.data.paging.EventsSummariesRemoteMediatorTest.Some of initial load weeks require refresh ## validate initialize()(EventsSummariesRemoteMediatorTest.kt:94)
	at java.base@11.0.13/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base@11.0.13/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base@11.0.13/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base@11.0.13/java.lang.reflect.Method.invoke(Method.java:566)
	at app//org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at app//org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at app//org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at app//org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at app//org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at app//org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at app//org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at app//org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at app//org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at app//org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at app//org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at app//org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at app//org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at app//org.junit.runners.Suite.runChild(Suite.java:128)
	at app//org.junit.runners.Suite.runChild(Suite.java:27)
	at app//org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at app//org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at app//org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at app//org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at app//org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at java.base@11.0.13/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base@11.0.13/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base@11.0.13/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base@11.0.13/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

Week is declared as

@JvmInline
internal value class Week @VisibleForTesting constructor(
    val firstDay: LocalDate
) : Comparable<Week>

It is passed to suspend function directly as simple parameter.

Kotlin version is 1.7.10

@qoomon
Copy link
Contributor

qoomon commented Aug 1, 2022

@aSemy I've created further tests, however I was not able fix it in away so that all test are passing. There are quite some location where value classes need to be handled or already be handled. Maybe you are capable to fix these tests.

class MockkTest {

    interface ServiceDummy {
        fun getAny(): Any
        fun getResult(): Result<Int>
        fun execute(block: ServiceDummy.() -> Int)
    }

    @Test
    fun `return Result for function with Result return type`() {
        val givenValue = Result.success(42)
        val service = mockk<ServiceDummy> {
            every { getResult() } returns givenValue
        }

        val result = service.getResult()

        assertEquals(givenValue, result)
        verify { dummy.getResult() }
    }

    @Test
    fun `return Result for function with Any return type`() {
        val givenValue = Result.success(42)
        val service = mockk<ServiceDummy> {
            every { getAny() } returns givenValue
        }

        val result = service.getAny()

        assertEquals(givenValue, result)
        verify { dummy.getAny() }
    }

    @Test
    fun `pass extension blocks as parameter`() {
        val givenValue = 42
        val dummy = mockk<ServiceDummy> {
            every { execute(any()) } answers {
                val block = arg<ServiceDummy.() -> Int>(0)
                this@mockk.block()
            }
        }

        val result = dummy.execute { givenValue }
        
        assertEquals(givenValue, result)
        verify { dummy.execute(any()) }
    }
}

@aSemy
Copy link
Contributor

aSemy commented Aug 1, 2022

thanks @qoomon. I'll look into this after #855 is done. I suspect something like the fix I proposed here #849 (comment) is needed, to ensure consistent behaviour.

I've also been pondering using some code generation (like Kotlin Poet) to generate tests for ALL possible combinations of value class args/responses, nullables, mock types, matcher types...

@qoomon
Copy link
Contributor

qoomon commented Aug 1, 2022

I've also been pondering using some code generation (like Kotlin Poet) to generate tests for ALL possible combinations of value class args/responses, nullables, mock types, matcher types...

That sounds interesting. I would be excited to see your solution.

@Raibaz
Copy link
Collaborator

Raibaz commented Aug 1, 2022

Yep that'd be pretty cool!

@fgubler
Copy link

fgubler commented Aug 2, 2023

Just to mention it: the workarounds mentioned above don't work if the Value-Class has a generic parameter.
For example

@JvmInline
value class NonEmptyList<out T> private constructor(val value: List<T>) : List<T> by value

fun foo(list: NonEmptyList<String>)

=> The method every { foo(anyValue()) } just runs will fail with the exception
"io.mockk.MockKException: Failed matching mocking signature for"

@j1c1m1b1
Copy link

Unfortunately, there is still an issue with this feature. If you have an instance that extends from a class with generic values, if the output value of an execution of a function is a value class instance a ClassCastException is thrown:

In this case ProductToProductInformationUiModelMapper extends from:

abstract class Mapper<in T, out R> {
    suspend fun map(parameter: T) : R
}

In this case R will be:

value class ProductItemUiModel(value: String) {
// Some value class validation
}
private val productToProductInformationUiModelMapper: ProductToProductInformationUiModelMapper = mockk()

private val thingToTest: ThingToTest = ThingToTest(productToProductInformationUiModelMapper)

@Test
fun `test somthing`() {
    val expectedValue = getSomeExpectedValue()
    val productItemUiModelValue = expectedValue.productItemUiModelValue
    val product = generateRandomProduct()

    coEvety { productToProductInformationUiModelMapper.map(product) } returns 
    ProductItemUiModel(productItemUiModelValue)

    runTest {
         val actualValue = thintToTest.doSomethingCool(product)
    }
}

Running this test throws:

class java.lang.String cannot be cast to class com.some.package.ProductInformationUiModel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
MockK features
  
To do
Development

Successfully merging a pull request may close this issue.