Skip to content

Commit 38cdda7

Browse files
committed
Add support of suspendable answers to BDD
1 parent 3173393 commit 38cdda7

File tree

4 files changed

+103
-12
lines changed

4 files changed

+103
-12
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ package org.mockito.kotlin
2828
import org.mockito.BDDMockito
2929
import org.mockito.BDDMockito.BDDMyOngoingStubbing
3030
import org.mockito.invocation.InvocationOnMock
31+
import org.mockito.kotlin.internal.SuspendableAnswer
3132
import org.mockito.stubbing.Answer
3233

3334
/**
@@ -65,6 +66,13 @@ infix fun <T> BDDMyOngoingStubbing<T>.willAnswer(value: (InvocationOnMock) -> T?
6566
return willAnswer { value(it) }
6667
}
6768

69+
/**
70+
* Alias for [BBDMyOngoingStubbing.willAnswer], accepting a suspend lambda.
71+
*/
72+
infix fun <T> BDDMyOngoingStubbing<T>.willSuspendableAnswer(value: suspend (InvocationOnMock) -> T?): BDDMockito.BDDMyOngoingStubbing<T> {
73+
return willAnswer(SuspendableAnswer(value))
74+
}
75+
6876
/**
6977
* Alias for [BBDMyOngoingStubbing.willReturn].
7078
*/

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

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

2828
import org.mockito.Mockito
29-
import org.mockito.internal.invocation.InterceptedInvocation
3029
import org.mockito.invocation.InvocationOnMock
30+
import org.mockito.kotlin.internal.SuspendableAnswer
3131
import org.mockito.stubbing.Answer
3232
import org.mockito.stubbing.OngoingStubbing
33-
import kotlin.coroutines.Continuation
34-
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
3533
import kotlin.reflect.KClass
3634

3735

@@ -128,13 +126,5 @@ infix fun <T> OngoingStubbing<T>.doAnswer(answer: (InvocationOnMock) -> T?): Ong
128126
}
129127

130128
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-
}
129+
return thenAnswer(SuspendableAnswer(answer))
140130
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2018 Niek Haarman
5+
* Copyright (c) 2007 Mockito contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
* THE SOFTWARE.
24+
*/
25+
26+
package org.mockito.kotlin.internal
27+
28+
import org.mockito.internal.invocation.InterceptedInvocation
29+
import org.mockito.invocation.InvocationOnMock
30+
import org.mockito.stubbing.Answer
31+
import kotlin.coroutines.Continuation
32+
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
33+
34+
/**
35+
* This class properly wraps suspendable lambda into [Answer]
36+
*/
37+
@Suppress("UNCHECKED_CAST")
38+
internal class SuspendableAnswer<T>(
39+
private val body: suspend (InvocationOnMock) -> T?
40+
) : Answer<T> {
41+
override fun answer(invocation: InvocationOnMock?): T {
42+
//all suspend functions/lambdas has Continuation as the last argument.
43+
//InvocationOnMock does not see last argument
44+
val rawInvocation = invocation as InterceptedInvocation
45+
val continuation = rawInvocation.rawArguments.last() as Continuation<T?>
46+
47+
// https://youtrack.jetbrains.com/issue/KT-33766#focus=Comments-27-3707299.0-0
48+
return body.startCoroutineUninterceptedOrReturn(invocation, continuation) as T
49+
}
50+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.mockito.kotlin
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlinx.coroutines.runBlocking
5+
import kotlinx.coroutines.withContext
6+
import org.junit.Test
7+
8+
import org.junit.Assert.*
9+
10+
class BDDMockitoKtTest {
11+
12+
@Test
13+
fun willSuspendableAnswer_withoutArgument() = runBlocking {
14+
val fixture: SomeInterface = mock()
15+
16+
given(fixture.suspending()).willSuspendableAnswer {
17+
withContext(Dispatchers.Default) { 42 }
18+
}
19+
20+
assertEquals(42, fixture.suspending())
21+
then(fixture).should().suspending()
22+
Unit
23+
}
24+
25+
@Test
26+
fun willSuspendableAnswer_witArgument() = runBlocking {
27+
val fixture: SomeInterface = mock()
28+
29+
given(fixture.suspendingWithArg(any())).willSuspendableAnswer {
30+
withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
31+
}
32+
33+
assertEquals(42, fixture.suspendingWithArg(42))
34+
then(fixture).should().suspendingWithArg(42)
35+
Unit
36+
}
37+
}
38+
39+
interface SomeInterface {
40+
41+
suspend fun suspending(): Int
42+
suspend fun suspendingWithArg(arg: Int): Int
43+
}

0 commit comments

Comments
 (0)