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

Unable to mock method accepting value class as a parameter #1170

Closed
mherda64 opened this issue Sep 26, 2023 · 1 comment · Fixed by #1192
Closed

Unable to mock method accepting value class as a parameter #1170

mherda64 opened this issue Sep 26, 2023 · 1 comment · Fixed by #1192

Comments

@mherda64
Copy link

mherda64 commented Sep 26, 2023

Expected Behavior

Mockk should be able to properly mock a method accepting a value class object as an argument using any() and return the expected value.

Current Behavior

Mockk is unable to properly mock the method, failing to match mocking signatures with left matchers: [any()].

Failed matching mocking signature for
io.mockk.MockKException: Failed matching mocking signature for
SignedCall(retValue=, isRetValueMock=false, retType=class kotlin.String, self=ExampleConsumer(#1), method=consume-Uezw9p0(UUID), args=[00000000-0000-0000-0000-000000000000], invocationStr=ExampleConsumer(#1).consume-Uezw9p0(00000000-0000-0000-0000-000000000000))
left matchers: [any()] ...

Support for value classes seems to be added here.

Steps to Reproduce

Run the example code snippet I provided below.

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.13.8 (the problem also occurs with 1.13.7 and JVM target 1.8)
  • OS: macOS 12.5
  • Kotlin version: 1.9.10
  • JDK version: 17
  • Kotest version: 5.4.2
  • Kotest-runner-junit5-jvm version: 4.6.0
  • Type of test: unit test

Stack trace

Failed matching mocking signature for
io.mockk.MockKException: Failed matching mocking signature for
SignedCall(retValue=, isRetValueMock=false, retType=class kotlin.String, self=ExampleConsumer(#1), method=consume-Uezw9p0(UUID), args=[00000000-0000-0000-0000-000000000000], invocationStr=ExampleConsumer(#1).consume-Uezw9p0(00000000-0000-0000-0000-000000000000))
left matchers: [any()]
	at io.mockk.impl.recording.SignatureMatcherDetector.detect(SignatureMatcherDetector.kt:97)
	at io.mockk.impl.recording.states.RecordingState.signMatchers(RecordingState.kt:39)
	at io.mockk.impl.recording.states.RecordingState.round(RecordingState.kt:31)
	at io.mockk.impl.recording.CommonCallRecorder.round(CommonCallRecorder.kt:50)
	at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:62)
	at io.mockk.impl.eval.EveryBlockEvaluator.every(EveryBlockEvaluator.kt:30)
	at io.mockk.MockKDsl.internalEvery(API.kt:94)
	at io.mockk.MockKKt.every(MockK.kt:143)
	at Test.beforeEach(Test.kt:38)
	at io.kotest.core.spec.ListenersKt$functionOverrideCallbacks$1.beforeEach(listeners.kt:54)
	at io.kotest.core.spec.LifecycleKt.invokeAllBeforeTestCallbacks(lifecycle.kt:74)
	at io.kotest.core.internal.TestCaseExecutor.executeActiveTest(TestCaseExecutor.kt:152)
	at io.kotest.core.internal.TestCaseExecutor.access$executeActiveTest(TestCaseExecutor.kt:57)
	at io.kotest.core.internal.TestCaseExecutor$intercept$innerExecute$1$1.invokeSuspend(TestCaseExecutor.kt:89)
	at io.kotest.core.internal.TestCaseExecutor$intercept$innerExecute$1$1.invoke(TestCaseExecutor.kt)
	at io.kotest.core.internal.TestCaseExecutor$intercept$innerExecute$1$1.invoke(TestCaseExecutor.kt)
	at io.kotest.core.internal.TestCaseExecutor.executeIfEnabled(TestCaseExecutor.kt:117)
	at io.kotest.core.internal.TestCaseExecutor.access$executeIfEnabled(TestCaseExecutor.kt:57)
	at io.kotest.core.internal.TestCaseExecutor$intercept$innerExecute$1.invokeSuspend(TestCaseExecutor.kt:89)
	at io.kotest.core.internal.TestCaseExecutor$intercept$innerExecute$1.invoke(TestCaseExecutor.kt)
	at io.kotest.core.internal.TestCaseExecutor$intercept$innerExecute$1.invoke(TestCaseExecutor.kt)
	at io.kotest.core.internal.TestCaseExecutor.intercept(TestCaseExecutor.kt:103)
	at io.kotest.core.internal.TestCaseExecutor.execute(TestCaseExecutor.kt:69)
	at io.kotest.engine.spec.runners.SingleInstanceSpecRunner.runTest(SingleInstanceSpecRunner.kt:104)
	at io.kotest.engine.spec.runners.SingleInstanceSpecRunner.access$runTest(SingleInstanceSpecRunner.kt:34)
	at io.kotest.engine.spec.runners.SingleInstanceSpecRunner$execute$interceptAndRun$2$4.invokeSuspend(SingleInstanceSpecRunner.kt:57)
	at io.kotest.engine.spec.runners.SingleInstanceSpecRunner$execute$interceptAndRun$2$4.invoke(SingleInstanceSpecRunner.kt)
	at io.kotest.engine.spec.runners.SingleInstanceSpecRunner$execute$interceptAndRun$2$4.invoke(SingleInstanceSpecRunner.kt)
	at io.kotest.engine.launchers.SequentialTestLauncher$launch$3$1$1.invokeSuspend(SequentialTestLauncher.kt:22)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:833)

Minimal reproducible code (the gist of this issue)

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.9.10"
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.kotest:kotest-assertions-json:5.4.2")
    implementation("io.mockk:mockk:1.13.8")
    implementation("io.kotest:kotest-runner-junit5-jvm:4.6.0")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "11"
}
import io.kotest.core.spec.style.FreeSpec
import io.kotest.core.test.TestCase
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import java.util.UUID

@JvmInline
value class Input(val value: UUID)

@JvmInline
value class Output(val value: String)

class ExampleConsumer() {
    fun consume(value: Input): Output =
        Output(value.toString())
}

class Test : FreeSpec() {

    init {
        "Should work" {
            // given
            val input = Input(UUID.randomUUID())

            // when
            val result = exampleConsumer.consume(input)

            // then
            result shouldBe Output("mockValue")
        }
    }

    private lateinit var exampleConsumer: ExampleConsumer

    override fun beforeEach(testCase: TestCase) {
        exampleConsumer = mockk {
            every { this@mockk.consume(any()) } answers { Output("mockValue") }
        }
    }
}
@milgner
Copy link
Contributor

milgner commented Dec 22, 2023

I encountered the same issue (already commented at the original PR).

After a cursory glance at the source code (with minimal understanding of what's going on 😅 ) it seems like the issue might be related to ChainedCallDetector::regularArgument. If I understand correctly, this method is supposed to detect the matched call and remove it from matcherMap if there's a match.

However, matcherMap contains one element with [Ref(Input@...)] whereas the signature created in regularArgument is [Ref(java.util.UUID)].

Looking at CommonRef::hashCode and the fact that matcherMap is explicitly set up as a HashMap, I figure that maybe the Refs are supposed to produce the same hash code here in order to match - but unfortunately they don't seem to.

Somehow I have a feeling that it's not related to value classes per se (after all, there's some explicit handling for them) but with value classes that wrap non-primitive objects. I.e. those for which InternalPlatform.isPassedByValue returns false.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants