Open kargath opened 2 years ago
Update: The issue seems to arise only when the mocked method is used within a runBlocking corroutine.
When returning the mock inside a "normal" piece of code, the mocking is done properly.
I'm adding a fully functional test:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.`should be equal to`
import org.junit.jupiter.api.Test
import org.mockito.kotlin.argWhere
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
class SimpleTest2 {
private val instance = MockSuspendClassImpl()
private val mockedInstance: MockSuspendClass = mock()
@Test
fun `test with corroutines simple - no mock`() {
// GIVEN
val data = Data("something")
val unit = UnderTest(instance)
// WHEN
val ret = unit.doSomethingPlain(data)
// THEN
ret `should be equal to` data.data
}
@Test
fun `test with corroutines data - no mock`() {
// GIVEN
val data = Data("something")
val unit = UnderTest(instance)
// WHEN
val ret = unit.doSomethingValue(data)
// THEN
ret `should be equal to` data
}
@Test
fun `test with corroutines simple - mock`() = runTest {
// GIVEN
val data = Data("something")
val unit = UnderTest(mockedInstance)
whenever(mockedInstance.printData(eqValueClass(data, { it.data }))) doReturn data.data
// WHEN
val ret = unit.doSomethingPlain(data)
// THEN
ret `should be equal to` data.data
}
@Test
fun `test with corroutines data - mock`() = runTest {
// GIVEN
val data = Data("something")
val unit = UnderTest(mockedInstance)
whenever(mockedInstance.printDataReturn(eqValueClass(data, { it.data }))) doReturn data
// WHEN
val ret = unit.doSomethingValue(data)
// THEN
ret `should be equal to` data
}
}
class UnderTest(val mockedClass: MockSuspendClass) {
fun doSomethingPlain(data: Data): String {
return runBlocking(Dispatchers.Default) { mockedClass.printData(data) }
}
fun doSomethingValue(data: Data): Data {
return runBlocking(Dispatchers.Default) { mockedClass.printDataReturn(data) }
}
}
interface MockSuspendClass {
suspend fun printData(data: Data): String
suspend fun printDataReturn(data: Data): Data
}
class MockSuspendClassImpl : MockSuspendClass {
override suspend fun printData(data: Data): String {
println("Data is $data")
delay(10)
return data.data
}
override suspend fun printDataReturn(data: Data): Data {
println("Data is $data")
delay(10)
return data
}
}
@JvmInline
value class Data(val data: String)
/**
* Workaround to support matching of value classes
*
* From: https://github.com/mockito/mockito-kotlin/issues/445#issuecomment-983619131
*/
inline fun <Outer, reified Inner> eqValueClass(
expected: Outer,
crossinline access: (Outer) -> Inner,
test: ((actual: Any) -> Boolean) -> Any? = ::argWhere
): Outer {
val assertion: (Any) -> Boolean = { actual ->
if (actual is Inner) {
access(expected) == actual
} else {
expected == actual
}
}
@Suppress("UNCHECKED_CAST")
return test(assertion) as Outer? ?: expected
}
Related issue https://youtrack.jetbrains.com/issue/KT-51641
does anybody have a workaround for this?
I'm trying to use any
currently, not even eq
.
Same issue here, it's really annoying.
For anyone else who found their way here, I found a workaround to using eq() and any() for an inline value class in #309
For me using the doSuspendableAnswer
method with delay
helps. So instead of doReturn(value)
I use
doSuspendableReturn {
delay(1)
value
}
Is this issue taken into work or it's root cause is known? It seems to be different from the one of https://github.com/mockito/mockito/pull/2280, as it is specific to the mocked method execution within a runBlocking
coroutine.
I've also discovered that the behaviour somehow depends on the value class
implementation. For instance, this test fails:
@JvmInline
value class Foo(
val value: String,
)
interface ToBeMocked {
suspend fun doSmth(): Foo
}
@Test
fun test() = runTest {
val expected = Foo("test")
val mock = mock<ToBeMocked> {
onBlocking { doSmth() } doReturn expected
}
val actual = mock.doSmth()
assertThat(actual).isEqualTo(expected)
}
While this one doesn't:
@JvmInline
value class Bar<out V, out E> internal constructor(
private val inlineValue: Any?,
)
interface ToBeMocked {
suspend fun doSmth(): Bar<String, Int>
}
@Test
fun test() = runTest {
val expected = Bar<String, Int>("test")
val mock = mock<ToBeMocked> {
onBlocking { doSmth() } doReturn expected
}
val actual = mock.doSmth()
assertThat(actual).isEqualTo(expected)
}
And the only difference is in value class
impl.
When returning a value class from a mocked method, class cast can not be done properly
Sample code:
When executing that code and getting the mock the response is as follows:
Tested returning a simple type like String (to discard matcher failing) and worked successfully.