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

eq() argument matcher for Kotlin suspend function call matches original call but will not match subsequent continuation resume calls #1869

Open
MonteCreasor opened this issue Feb 1, 2020 · 1 comment

Comments

@MonteCreasor
Copy link

Version Mockito 3.2.4
JDK 8
Ubuntu 18.04
Test run in Android Studio IDE 3.5.3

Using the eq() argument matcher when calling any Kotlin suspend function will always match the first (original) call, but will fail to match any continuation resume calls made by the Kotlin coroutine framework.

However, if you change the eq() to any() or anyInt() (if your function argument is an Int value), then the argument matcher will work for the original call and all subsequent continuation resume calls.

I realize that there is an extra hidden parameter but it seems to me that if eq() succeeds in the original test call, it should also succeed for the continuation resume calls.

The following 2 test in the test class provided below will produce TooFewActualInvocations for the 2nd test:

Test output:

org.mockito.exceptions.verification.TooFewActualInvocations: 
foobar.foobar(4);
Wanted 5 times:
-> at Example.$Foobar.foobar(Example.kt:15)
But was 1 time:
-> at Example$callFoobar$2$job$1.invokeSuspend(Example.kt:52)

Compilable test class that uses yield() calls to force 4 continuation resume calls:

import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.*

class Example {
    companion object {
        const val YIELDS = 4

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

    private val foobarMock = mock(Foobar::class.java)

    class Foobar {
        suspend fun foobar(spins: Int) {
            repeat(spins) {
                yield()
            }
        }
    }

    @Test
    fun `matcher foobar(anyInt()) matches original call and all continuation resume calls`() {
        runBlocking {
            `when`(foobarMock.foobar(anyInt())).thenCallRealMethod()

            callFoobar()

            // Total calls should be first call plus the number of yields.
            verify(foobarMock, times(1 + YIELDS)).foobar(anyInt())
        }
    }

    @Test
    fun `matcher foobar(eq(Int)) matches original call but NO continuation resume calls`() {
        runBlocking {
            `when`(foobarMock.foobar(eq(YIELDS))).thenCallRealMethod()

            callFoobar()

            // Total calls should be first call plus the number of yields.
            verify(foobarMock, times(1 + YIELDS)).foobar(eq(YIELDS))
        }
    }

    suspend fun callFoobar() = coroutineScope {
        val job = launch { 
           foobarMock.foobar(YIELDS) 
        }
        
        repeat(YIELDS) { yield() }
        
        job.join()
    }
}
@ChristianSchwarz
Copy link
Contributor

@MonteCreasor better use MockK in a Kotlin project.

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

No branches or pull requests

2 participants