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

Testing suspending function types fails when invocation is done in different context #247

Open
matejdro opened this issue Apr 11, 2018 · 21 comments

Comments

@matejdro
Copy link
Contributor

Run this test code:

val suspendFunction : suspend () -> Unit = mock()

runBlocking {
    suspendFunction.invoke()
}

runBlocking {
    verify(suspendFunction).invoke()
}

It should succeed (function was invoked, so verify should pass), but it fails with an error:

Argument(s) are different! Wanted:
function1.invoke(
    (testLambda$2) Function2<kotlinx.coroutines.experimental.CoroutineScope, kotlin.coroutines.experimental.Continuation<? super kotlin.Unit>, java.lang.Object>
);
-> at LambdaTest$testLambda$2.doResume(TestTest.kt:19)
Actual invocation has different arguments:
function1.invoke(
    (testLambda$1) Function2<kotlinx.coroutines.experimental.CoroutineScope, kotlin.coroutines.experimental.Continuation<? super kotlin.Unit>, java.lang.Object>
);

Reproduced on versions:

ext.kotlin_version = '1.2.31'
compile "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5"
testCompile 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.0.0-alpha03'
@bohsen
Copy link

bohsen commented Apr 24, 2018

This is not caused by mockito-kotlin. Its the way the coroutines are used. You start two different coroutines by using runblocking twice. This will create two different coroutines with different contexts.

The right way would be to run it in one coroutine (this will pass):

@Test
fun mockSuspendingFunction() = runBlocking {
    val suspendFunction : suspend () -> Unit = mock()
    suspendFunction()
    verify(suspendFunction).invoke()
}

bohsen added a commit to bohsen/mockito-kotlin that referenced this issue Apr 24, 2018
bohsen added a commit to bohsen/mockito-kotlin that referenced this issue Apr 24, 2018
bohsen added a commit to bohsen/mockito-kotlin that referenced this issue Apr 24, 2018
@matejdro
Copy link
Contributor Author

matejdro commented Apr 24, 2018

I don't think "the right way to use coroutines" matters here.

verify without arguments is supposed to verify that method has been called, right? It should not care in what way the method was called.

And in my example it HAS been called. So that test should succeed.

@bohsen
Copy link

bohsen commented Apr 24, 2018

Add logging to your example and see for yourself, ie.

@Test
fun mockSuspendingFunction() {

    val suspendFunction = mock<SuspendType>()

    runBlocking {
        log("I'm invoking a piece of the answer")
        suspendFunction.invoke()
    }

    runBlocking {
        log("I'm verifying the invocation")
        verify(suspendFunction).invoke()
    }
}

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

Remember to add debugging JVM option.

The test will fail and print the following followed by the stacktrace:

[main @coroutine#1] I'm invoking a piece of the answer
[main @coroutine#2] I'm verifying the invocation
... stacktrace ...

You will notice that you are inside two different coroutines which can be compared with running in different threads.

@matejdro
Copy link
Contributor Author

matejdro commented Apr 25, 2018

Yes, I understand that. But mockito is supposed to verify if method is called, right? It shouldn't care on what context the method is called.

For example verify() always works for normal methods regardless of on what threads they are called. Why wouldn't same hold for coroutines?

Example in the first post is of course not real-world one. I know it is stupid to call runBlocking twice, but thist is just shortest example I could make for bug report. In real world, need to verify method call from different context is not really that uncommon.

bohsen added a commit to bohsen/mockito-kotlin that referenced this issue Jun 14, 2018
@flavioarfaria
Copy link

Maybe related to this: #205

@mtrewartha
Copy link

I would agree with this, especially given the point about threads that @matejdro made and the fact that this library is written to make working with Mockito and Kotlin easier. Here's another example of this failing:

package com.example

import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.runBlocking
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe

interface SomeInterface {
    suspend fun someSuspendFunction(): Boolean
}

class SuspendSpec : Spek({

    val mockInterface by memoized { mock<SomeInterface>() }

    describe("stubbing a suspend function") {

        beforeEachTest {
            runBlocking { whenever(mockInterface.someSuspendFunction()) }.thenReturn(true)
        }

        it("gives you the return value that you asked for") {
            val returnValue: Boolean? = runBlocking { mockInterface.someSuspendFunction() }
            assertThat(returnValue).isTrue()
        }

        it("lets you verify invocation of the stubbed function") {
            runBlocking { mockInterface.someSuspendFunction() }
            runBlocking { verify(mockInterface).someSuspendFunction() }
        }
    }
})

Both specs fail, but should pass IMO:

expected: true
but was : null
	at com.example.SuspendSpec$1$1$2.invoke(SuspendSpec.kt:27)
	at com.example.SuspendSpec$1$1$2.invoke(SuspendSpec.kt:15)
	at org.spekframework.spek2.runtime.scope.TestScopeImpl.execute(Scopes.kt:87)
	at org.spekframework.spek2.runtime.Executor.execute(Executor.kt:28)
	at org.spekframework.spek2.runtime.Executor.execute(Executor.kt:31)
	at org.spekframework.spek2.runtime.Executor.execute(Executor.kt:31)
	at org.spekframework.spek2.runtime.Executor.execute(Executor.kt:15)
	at org.spekframework.spek2.runtime.AbstractRuntime.execute(SpekRuntime.kt:32)
	at org.spekframework.spek2.junit.SpekTestEngine.execute(SpekTestEngine.kt:55)
	at [[Testing framework: 3 frames collapsed (https://goo.gl/aH3UyP)]].(:0)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:92)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$100(JUnitPlatformTestClassProcessor.java:77)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:73)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
	at [[Reflective call: 4 frames collapsed (https://goo.gl/aH3UyP)]].(:0)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
	at com.sun.proxy.$Proxy2.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:131)
	at [[Reflective call: 4 frames collapsed (https://goo.gl/aH3UyP)]].(:0)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:155)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:137)
	at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:404)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
	at java.lang.Thread.run(Thread.java:748)
Argument(s) are different! Wanted:
someInterface.someSuspendFunction(
    Continuation @ com.example.SuspendSpec$1$1$3$2
);
-> at com.example.SuspendSpec$1$1$3$2.invokeSuspend(SuspendSpec.kt:32)
Actual invocation has different arguments:
someInterface.someSuspendFunction(
    Continuation @ com.example.SuspendSpec$1$1$3$1
);
-> at com.example.SuspendSpec$1$1$3$1.invokeSuspend(SuspendSpec.kt:31)

	at com.example.SuspendSpec$1$1$3$2.invokeSuspend(SuspendSpec.kt:32)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
	at kotlinx.coroutines.DispatchedTask$DefaultImpls.run(Dispatched.kt:147)
	at kotlinx.coroutines.DispatchedContinuation.run(Dispatched.kt:14)
	at kotlinx.coroutines.EventLoopBase.processNextEvent(EventLoop.kt:136)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:133)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:52)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:41)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at com.example.SuspendSpec$1$1$3.invoke(SuspendSpec.kt:32)
	at com.example.SuspendSpec$1$1$3.invoke(SuspendSpec.kt:15)
	at org.spekframework.spek2.runtime.scope.TestScopeImpl.execute(Scopes.kt:87)
	at org.spekframework.spek2.runtime.Executor.execute(Executor.kt:28)
	at org.spekframework.spek2.runtime.Executor.execute(Executor.kt:31)
	at org.spekframework.spek2.runtime.Executor.execute(Executor.kt:31)
	at org.spekframework.spek2.runtime.Executor.execute(Executor.kt:15)
	at org.spekframework.spek2.runtime.AbstractRuntime.execute(SpekRuntime.kt:32)
	at org.spekframework.spek2.junit.SpekTestEngine.execute(SpekTestEngine.kt:55)
	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 org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:92)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$100(JUnitPlatformTestClassProcessor.java:77)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:73)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
	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.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
	at com.sun.proxy.$Proxy2.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:131)
	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.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:155)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:137)
	at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:404)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
	at java.lang.Thread.run(Thread.java:748)

bohsen added a commit to bohsen/mockito-kotlin that referenced this issue Sep 6, 2018
@bohsen
Copy link

bohsen commented Sep 10, 2018

This is not a bug in Mockito-kotlin. I still believe it comes down to how coroutines work.

Suspending functions are not some special type of functions. They are regular functions that use CPS transformation to add an extra parameter of type [Continuation](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/-continuation/index.html) (a classical generic callback). Have a look at this.

So for each invocation of runBlocking { ... } you have different Continuation's passed into your function.

The stacktrace actually tells you this.

Argument(s) are different! Wanted:
someInterface.someSuspendFunction(
    Continuation @ com.example.SuspendSpec$1$1$3$2
);
-> at com.example.SuspendSpec$1$1$3$2.invokeSuspend(SuspendSpec.kt:32)
Actual invocation has different arguments:
someInterface.someSuspendFunction(
    Continuation @ com.example.SuspendSpec$1$1$3$1

So just to recap. I don't believe this is an issue for mockito-kotlin to handle, but should be discussed in the coroutine project. Though I actually don't believe there would ever exist an use-case for this, as to me it wouldn't ever make sense to call runBlocking twice in the same context.

@matejdro
Copy link
Contributor Author

matejdro commented Sep 10, 2018

I don't believe this is an issue for mockito-kotlin to handle, but should be discussed in the coroutine project

I'm not sure coroutines people can change anything, this is how they made their system. If anything, it should be discussed within regular mockito project. But mockito is java-only and does not really involve kotlin, so next best thing is this project.

I propose solution that somehow ignores first argument here (or automatically marks it as any() somehow), probably through Kotlin-specific reflection. But I do not know enough of mockito's internals to be any more practical.

as to me it wouldn't ever make sense to call runBlocking twice in the same context

As already stated before, example in the first post is NOT production example. I agree it is completely useless. But at the same time, it is the most simple example one can make for a bug report. This is meant for generic bug report for all verifications with different continuations.

@mtrewartha
Copy link

mtrewartha commented Sep 11, 2018

We can debate what to call this ("bug" or "not a bug") all we want, but it doesn't really matter. The important thing is that we're looking for a way to mock and verify suspend functions. Given that suspend functions are a Kotlin concept, and we're trying to mock/verify them, a Mockito concept, it seems rather fitting that mockito-kotlin provide a way to do this.

EDIT: If there's a way to do this that I'm just not aware of, I'm all ears!

@bohsen
Copy link

bohsen commented Sep 11, 2018

EDIT: If there's a way to do this that I'm just not aware of, I'm all ears!

@miketrewartha There is a way - take a look at the tests in the source code.

@mtrewartha
Copy link

mtrewartha commented Sep 11, 2018

@bohsen all of those make sense, but I'm looking for a way to mock a suspend function for a mock I created further up in a test (in some contextual setup), because I don't want to have to redefine everything inside each test or before block:

object SubjectClassSpec : Spek({
    val mockDependency by memoized { mock<DependencyClass>() }
    val subject by memoized { SubjectClass(mockDependency) }

    describe("doing something") {
        beforeEachTest {
            // This doesn't work (not calling someSuspendFunction in a suspend function or coroutine), 
            // so how can I mock this function without having to redefine my subject and mockDependency
            // in this specific beforeEachTest block?
            whenever(mockDependency.someSuspendFunction()).thenReturn("some value")
        }
        // ... some actual assertions in `it` blocks
    }
})

I've tried wrapping the whenever(...) call in a runBlocking { ... }, but null is still being returned instead of "some value" for some reason.

@mtrewartha
Copy link

Here's another example of something related that I'm stuck on:

object SubjectClassSpec : Spek({

    val mockDependency by memoized { mock<DependencyClass>() }
    val subject by memoized { SubjectClass(mockDependency) }

    describe("doing something") {
        val result = subject.doSomethingThatCallsTheDependencySuspendFunction()

        it("uses the dependency in a certain way") {
            runBlocking { verify(mockDependency).someSuspendFunction() }
        }
    }
})

This results in a test failure because the continuations aren't the same. Now that I think about is, I'm wondering if that's a problem that's specific to my setup. I have a class that follows the same general pattern as above and in my actual function (represented by doSomethingThatCallsTheDependencySuspendFunction() here), I have a runBlocking { ... } that wraps the suspend function's call. Unfortunately, the function that calls the suspend function is part of a Java interface, so I can't mark it as a suspend function.

@bohsen
Copy link

bohsen commented Sep 12, 2018

Should work even with a Java interface I believe. Try wrapping
val result = subject.doSomethingThatCallsTheDependencySuspendFunction() with `runBlocking {}``

Like this (not using Java interface though, but shouldn't matter):

object SpekTestCoroutines : Spek({

    val mockDependency by memoized { mock<Foo>() }
    val subject by memoized { Bar(mockDependency) }

    describe("doing something") {
        runBlocking {
            subject.result(42)

            it("uses the dependency in a certain way") {
                runBlocking { verify(mockDependency).suspending() }
            }
        }
    }
})

interface Foo {
    suspend fun suspending(): Int
    fun nonsuspending(): Int
}

class Bar(val foo: Foo) {
    suspend fun result(r: Int) = withContext(CommonPool) { foo.suspending() }
}

This is using spek 2.0.0-alpha.2. Here's a section of my build.gradle:

dependencies {
    ...
    testCompile 'org.junit.jupiter:junit-jupiter-api:5.2.0'

    testImplementation ('org.spekframework.spek2:spek-dsl-jvm:2.0.0-alpha.2')  {
        exclude group: 'org.jetbrains.kotlin'
    }
    testRuntimeOnly ('org.spekframework.spek2:spek-runner-junit5:2.0.0-alpha.2') {
        exclude group: 'org.junit.platform'
        exclude group: 'org.jetbrains.kotlin'
    }

    // spek requires kotlin-reflect, can be omitted if already in the classpath
    testRuntimeOnly "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}

// setup the test task
test {
    useJUnitPlatform {
        includeEngines 'spek2'
    }
}

@Ethan1983
Copy link

It works with the new version of mockito-kotlin,

testCompile 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.0.0'

@matejdro
Copy link
Contributor Author

matejdro commented Feb 4, 2019

Still broken for me with com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0, Kotlin 1.3.20 and coroutines-android 1.1.1

@mochadwi
Copy link

mochadwi commented Mar 29, 2020

have you upgrade to use 2.2.0? @matejdro In my case, suspending function now works when using 2.2.0

thanks @nhaarman

@matejdro
Copy link
Contributor Author

Sorry, I'm not using mockito-kotlin anymore so I cannot confirm.

@danielesegato
Copy link

I'm also experienced this in mockito-kotlin 3.2.0.

I've an interface

interface Example {
  val onSubmit: suspend () -> Unit
}

I need to mock this interface

val mockedOnSubmit = mock<suspend () -> Unit>()
val mockedExample = mock<Example> {
  on { onSubmit } doReturn mockedOnSubmit
}

the verify on mockedOnSubmit fails complaining the continuation is different.

verify(mockedOnSubmit, times(1)).invoke()

Any input on how to test in this situation? I really don't care the continuation is different, of course it is. I just want to check it is invoked.

@kyodgorbek
Copy link

kyodgorbek commented Aug 24, 2021 via email

@danielesegato
Copy link

@kyodgorbek what is it that you are asking me if I used in that article?

I didn't use that article. But I do have my own TestCoroutineDispatcher rule.

Can you elaborate on what you think is the cause?

By the way... if I change

interface Example {
  val onSubmit: suspend () -> Unit
}

to

interface Example {
  suspend fun onSubmit()
}

it is mockable and works like I expect, so I'm not sure why the lambda wouldn't

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

No branches or pull requests

9 participants