Skip to content

Commit

Permalink
Add suspendable answer (#357)
Browse files Browse the repository at this point in the history
  • Loading branch information
neworld authored Apr 1, 2021
1 parent 9c6e234 commit 3173393
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 2 deletions.
2 changes: 1 addition & 1 deletion mockito-kotlin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies {
testCompile 'com.nhaarman:expect.kt:1.0.0'

testCompile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testCompile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
testCompile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
package org.mockito.kotlin

import org.mockito.Mockito
import org.mockito.internal.invocation.InterceptedInvocation
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import org.mockito.stubbing.OngoingStubbing
import kotlin.DeprecationLevel.ERROR
import kotlin.coroutines.Continuation
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
import kotlin.reflect.KClass


Expand Down Expand Up @@ -124,3 +126,15 @@ infix fun <T> OngoingStubbing<T>.doAnswer(answer: Answer<*>): OngoingStubbing<T>
infix fun <T> OngoingStubbing<T>.doAnswer(answer: (InvocationOnMock) -> T?): OngoingStubbing<T> {
return thenAnswer(answer)
}

infix fun <T> OngoingStubbing<T>.doSuspendableAnswer(answer: suspend (InvocationOnMock) -> T?): OngoingStubbing<T> {
return thenAnswer {
//all suspend functions/lambdas has Continuation as the last argument.
//InvocationOnMock does not see last argument
val rawInvocation = it as InterceptedInvocation
val continuation = rawInvocation.rawArguments.last() as Continuation<T?>

// https://youtrack.jetbrains.com/issue/KT-33766#focus=Comments-27-3707299.0-0
answer.startCoroutineUninterceptedOrReturn(it, continuation)
}
}
99 changes: 99 additions & 0 deletions mockito-kotlin/src/test/kotlin/test/CoroutinesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.actor
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mockito.kotlin.*
import java.util.*


class CoroutinesTest {
Expand Down Expand Up @@ -157,11 +161,106 @@ class CoroutinesTest {
verify(testSubject).suspending()
}
}

@Test
fun answerWithSuspendFunction() = runBlocking {
val fixture: SomeInterface = mock()

whenever(fixture.suspendingWithArg(any())).doSuspendableAnswer {
withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
}

assertEquals(5, fixture.suspendingWithArg(5))
}

@Test
fun inplaceAnswerWithSuspendFunction() = runBlocking {
val fixture: SomeInterface = mock {
onBlocking { suspendingWithArg(any()) } doSuspendableAnswer {
withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
}
}

assertEquals(5, fixture.suspendingWithArg(5))
}

@Test
fun callFromSuspendFunction() = runBlocking {
val fixture: SomeInterface = mock()

whenever(fixture.suspendingWithArg(any())).doSuspendableAnswer {
withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
}

val result = async {
val answer = fixture.suspendingWithArg(5)

Result.success(answer)
}

assertEquals(5, result.await().getOrThrow())
}

@Test
fun callFromActor() = runBlocking {
val fixture: SomeInterface = mock()

whenever(fixture.suspendingWithArg(any())).doSuspendableAnswer {
withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
}

val actor = actor<Optional<Int>> {
for (element in channel) {
fixture.suspendingWithArg(element.get())
}
}

actor.send(Optional.of(10))
actor.close()

verify(fixture).suspendingWithArg(10)

Unit
}

@Test
fun answerWithSuspendFunctionWithoutArgs() = runBlocking {
val fixture: SomeInterface = mock()

whenever(fixture.suspending()).doSuspendableAnswer {
withContext(Dispatchers.Default) { 42 }
}

assertEquals(42, fixture.suspending())
}

@Test
fun willAnswerWithControlledSuspend() = runBlocking {
val fixture: SomeInterface = mock()

val job = Job()

whenever(fixture.suspending()).doSuspendableAnswer {
job.join()
5
}

val asyncTask = async {
fixture.suspending()
}

job.complete()

withTimeout(100) {
assertEquals(5, asyncTask.await())
}
}
}

interface SomeInterface {

suspend fun suspending(): Int
suspend fun suspendingWithArg(arg: Int): Int
fun nonsuspending(): Int
}

Expand Down

0 comments on commit 3173393

Please sign in to comment.