Skip to content

File tree

3 files changed

+115
-2
lines changed

3 files changed

+115
-2
lines changed

mockito-kotlin/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ dependencies {
3131
testCompile 'com.nhaarman:expect.kt:1.0.0'
3232

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

3636
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0"
3737
}

mockito-kotlin/src/main/kotlin/org/mockito/kotlin/OngoingStubbing.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
package org.mockito.kotlin
2727

2828
import org.mockito.Mockito
29+
import org.mockito.internal.invocation.InterceptedInvocation
2930
import org.mockito.invocation.InvocationOnMock
3031
import org.mockito.stubbing.Answer
3132
import org.mockito.stubbing.OngoingStubbing
32-
import kotlin.DeprecationLevel.ERROR
33+
import kotlin.coroutines.Continuation
34+
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
3335
import kotlin.reflect.KClass
3436

3537

@@ -124,3 +126,15 @@ infix fun <T> OngoingStubbing<T>.doAnswer(answer: Answer<*>): OngoingStubbing<T>
124126
infix fun <T> OngoingStubbing<T>.doAnswer(answer: (InvocationOnMock) -> T?): OngoingStubbing<T> {
125127
return thenAnswer(answer)
126128
}
129+
130+
infix fun <T> OngoingStubbing<T>.doSuspendableAnswer(answer: suspend (InvocationOnMock) -> T?): OngoingStubbing<T> {
131+
return thenAnswer {
132+
//all suspend functions/lambdas has Continuation as the last argument.
133+
//InvocationOnMock does not see last argument
134+
val rawInvocation = it as InterceptedInvocation
135+
val continuation = rawInvocation.rawArguments.last() as Continuation<T?>
136+
137+
// https://youtrack.jetbrains.com/issue/KT-33766#focus=Comments-27-3707299.0-0
138+
answer.startCoroutineUninterceptedOrReturn(it, continuation)
139+
}
140+
}

mockito-kotlin/src/test/kotlin/test/CoroutinesTest.kt

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import kotlinx.coroutines.Dispatchers
77
import kotlinx.coroutines.delay
88
import kotlinx.coroutines.runBlocking
99
import kotlinx.coroutines.withContext
10+
import kotlinx.coroutines.*
11+
import kotlinx.coroutines.channels.actor
12+
import org.junit.Assert.assertEquals
1013
import org.junit.Test
1114
import org.mockito.kotlin.*
15+
import java.util.*
1216

1317

1418
class CoroutinesTest {
@@ -157,11 +161,106 @@ class CoroutinesTest {
157161
verify(testSubject).suspending()
158162
}
159163
}
164+
165+
@Test
166+
fun answerWithSuspendFunction() = runBlocking {
167+
val fixture: SomeInterface = mock()
168+
169+
whenever(fixture.suspendingWithArg(any())).doSuspendableAnswer {
170+
withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
171+
}
172+
173+
assertEquals(5, fixture.suspendingWithArg(5))
174+
}
175+
176+
@Test
177+
fun inplaceAnswerWithSuspendFunction() = runBlocking {
178+
val fixture: SomeInterface = mock {
179+
onBlocking { suspendingWithArg(any()) } doSuspendableAnswer {
180+
withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
181+
}
182+
}
183+
184+
assertEquals(5, fixture.suspendingWithArg(5))
185+
}
186+
187+
@Test
188+
fun callFromSuspendFunction() = runBlocking {
189+
val fixture: SomeInterface = mock()
190+
191+
whenever(fixture.suspendingWithArg(any())).doSuspendableAnswer {
192+
withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
193+
}
194+
195+
val result = async {
196+
val answer = fixture.suspendingWithArg(5)
197+
198+
Result.success(answer)
199+
}
200+
201+
assertEquals(5, result.await().getOrThrow())
202+
}
203+
204+
@Test
205+
fun callFromActor() = runBlocking {
206+
val fixture: SomeInterface = mock()
207+
208+
whenever(fixture.suspendingWithArg(any())).doSuspendableAnswer {
209+
withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
210+
}
211+
212+
val actor = actor<Optional<Int>> {
213+
for (element in channel) {
214+
fixture.suspendingWithArg(element.get())
215+
}
216+
}
217+
218+
actor.send(Optional.of(10))
219+
actor.close()
220+
221+
verify(fixture).suspendingWithArg(10)
222+
223+
Unit
224+
}
225+
226+
@Test
227+
fun answerWithSuspendFunctionWithoutArgs() = runBlocking {
228+
val fixture: SomeInterface = mock()
229+
230+
whenever(fixture.suspending()).doSuspendableAnswer {
231+
withContext(Dispatchers.Default) { 42 }
232+
}
233+
234+
assertEquals(42, fixture.suspending())
235+
}
236+
237+
@Test
238+
fun willAnswerWithControlledSuspend() = runBlocking {
239+
val fixture: SomeInterface = mock()
240+
241+
val job = Job()
242+
243+
whenever(fixture.suspending()).doSuspendableAnswer {
244+
job.join()
245+
5
246+
}
247+
248+
val asyncTask = async {
249+
fixture.suspending()
250+
}
251+
252+
job.complete()
253+
254+
withTimeout(100) {
255+
assertEquals(5, asyncTask.await())
256+
}
257+
}
160258
}
161259

162260
interface SomeInterface {
163261

164262
suspend fun suspending(): Int
263+
suspend fun suspendingWithArg(arg: Int): Int
165264
fun nonsuspending(): Int
166265
}
167266

0 commit comments

Comments
 (0)