Skip to content

Commit 8149507

Browse files
committed
Improving ongoing stubbing support for suspend functions, by introducing CoroutineAwareAnswer
1 parent c523b9c commit 8149507

25 files changed

+2107
-1013
lines changed

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ import org.mockito.BDDMockito
3131
import org.mockito.BDDMockito.BDDMyOngoingStubbing
3232
import org.mockito.BDDMockito.Then
3333
import org.mockito.invocation.InvocationOnMock
34-
import org.mockito.kotlin.internal.SuspendableAnswer
34+
import org.mockito.kotlin.internal.CoroutineAwareAnswer
3535
import org.mockito.stubbing.Answer
3636
import kotlin.reflect.KClass
3737

3838
/**
3939
* Alias for [BDDMockito.given].
4040
*/
41-
fun <T> given(methodCall: T): BDDMockito.BDDMyOngoingStubbing<T> {
41+
fun <T> given(methodCall: T): BDDMyOngoingStubbing<T> {
4242
return BDDMockito.given(methodCall)
4343
}
4444

@@ -55,14 +55,14 @@ fun <T> given(methodCall: () -> T): BDDMyOngoingStubbing<T> {
5555
* Warning: Only last method call can be stubbed in the function.
5656
* other method calls are ignored!
5757
*/
58-
fun <T> givenBlocking(methodCall: suspend CoroutineScope.() -> T): BDDMockito.BDDMyOngoingStubbing<T> {
58+
fun <T> givenBlocking(methodCall: suspend CoroutineScope.() -> T): BDDMyOngoingStubbing<T> {
5959
return runBlocking { BDDMockito.given(methodCall()) }
6060
}
6161

6262
/**
6363
* Alias for [BDDMockito.then].
6464
*/
65-
fun <T> then(mock: T): BDDMockito.Then<T> {
65+
fun <T> then(mock: T): Then<T> {
6666
return BDDMockito.then(mock)
6767
}
6868

@@ -77,35 +77,35 @@ fun <T, R> Then<T>.shouldBlocking(f: suspend T.() -> R): R {
7777
/**
7878
* Alias for [BDDMyOngoingStubbing.will]
7979
* */
80-
infix fun <T> BDDMyOngoingStubbing<T>.will(value: Answer<T>): BDDMockito.BDDMyOngoingStubbing<T> {
80+
infix fun <T> BDDMyOngoingStubbing<T>.will(value: Answer<T>): BDDMyOngoingStubbing<T> {
8181
return will(value)
8282
}
8383

8484
/**
85-
* Alias for [BBDMyOngoingStubbing.willAnswer], accepting a lambda.
85+
* Alias for [BDDMyOngoingStubbing.willAnswer], accepting a lambda.
8686
*/
87-
infix fun <T> BDDMyOngoingStubbing<T>.willAnswer(value: (InvocationOnMock) -> T?): BDDMockito.BDDMyOngoingStubbing<T> {
88-
return willAnswer { value(it) }
87+
infix fun <T> BDDMyOngoingStubbing<T>.willAnswer(answer: (InvocationOnMock) -> T?): BDDMyOngoingStubbing<T> {
88+
return willAnswer { answer(it) }
8989
}
9090

9191
/**
92-
* Alias for [BBDMyOngoingStubbing.willAnswer], accepting a suspend lambda.
92+
* Alias for [BDDMyOngoingStubbing.willAnswer], accepting a suspend lambda.
9393
*/
94-
infix fun <T> BDDMyOngoingStubbing<T>.willSuspendableAnswer(value: suspend (InvocationOnMock) -> T?): BDDMockito.BDDMyOngoingStubbing<T> {
95-
return willAnswer(SuspendableAnswer(value))
94+
infix fun <T> BDDMyOngoingStubbing<T>.willSuspendableAnswer(answer: suspend (KInvocationOnMock) -> T?): BDDMyOngoingStubbing<T> {
95+
return willAnswer(CoroutineAwareAnswer(answer))
9696
}
9797

9898
/**
99-
* Alias for [BBDMyOngoingStubbing.willReturn].
99+
* Alias for [BDDMyOngoingStubbing.willReturn].
100100
*/
101-
infix fun <T> BDDMyOngoingStubbing<T>.willReturn(value: () -> T): BDDMockito.BDDMyOngoingStubbing<T> {
101+
infix fun <T> BDDMyOngoingStubbing<T>.willReturn(value: () -> T): BDDMyOngoingStubbing<T> {
102102
return willReturn(value())
103103
}
104104

105105
/**
106-
* Alias for [BBDMyOngoingStubbing.willThrow].
106+
* Alias for [BDDMyOngoingStubbing.willThrow].
107107
*/
108-
infix fun <T> BDDMyOngoingStubbing<T>.willThrow(value: () -> Throwable): BDDMockito.BDDMyOngoingStubbing<T> {
108+
infix fun <T> BDDMyOngoingStubbing<T>.willThrow(value: () -> Throwable): BDDMyOngoingStubbing<T> {
109109
return willThrow(value())
110110
}
111111

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,67 @@ package org.mockito.kotlin
2828
import org.mockito.invocation.InvocationOnMock
2929

3030
class KInvocationOnMock(
31-
private val invocationOnMock: InvocationOnMock
31+
val invocationOnMock: InvocationOnMock
3232
) : InvocationOnMock by invocationOnMock {
3333

3434
operator fun <T> component1(): T = invocationOnMock.getArgument(0)
3535
operator fun <T> component2(): T = invocationOnMock.getArgument(1)
3636
operator fun <T> component3(): T = invocationOnMock.getArgument(2)
3737
operator fun <T> component4(): T = invocationOnMock.getArgument(3)
3838
operator fun <T> component5(): T = invocationOnMock.getArgument(4)
39+
40+
/**
41+
* The first argument.
42+
* @throws IndexOutOfBoundsException if the argument is not available.
43+
*/
44+
inline fun <reified T> first(): T = invocationOnMock.getArgument(0)
45+
46+
/**
47+
* The second argument.
48+
* @throws IndexOutOfBoundsException if the argument is not available.
49+
*/
50+
inline fun <reified T> second(): T = invocationOnMock.getArgument(1)
51+
52+
/**
53+
* The third argument.
54+
* @throws IndexOutOfBoundsException if the argument is not available.
55+
*/
56+
inline fun <reified T> third(): T = invocationOnMock.getArgument(2)
57+
58+
/**
59+
* The fourth argument.
60+
* @throws IndexOutOfBoundsException if the argument is not available.
61+
*/
62+
inline fun <reified T> fourth(): T = invocationOnMock.getArgument(3)
63+
64+
/**
65+
* The fifth argument.
66+
* @throws IndexOutOfBoundsException if the argument is not available.
67+
*/
68+
inline fun <reified T> fifth(): T = invocationOnMock.getArgument(4)
69+
70+
/**
71+
* The last argument.
72+
* @throws IndexOutOfBoundsException if the argument is not available.
73+
*/
74+
inline fun <reified T> last(): T {
75+
val size = invocationOnMock.arguments.size
76+
require(size >= 1) { "The invocation was expected to have at least 1 argument but got none." }
77+
return invocationOnMock.getArgument(size - 1)
78+
}
79+
80+
/**
81+
* The single argument.
82+
* @throws IndexOutOfBoundsException if the argument is not available.
83+
*/
84+
inline fun <reified T> single(): T {
85+
val size = invocationOnMock.arguments.size
86+
require(size == 1) { "The invocation was expected to have exactly 1 argument but got $size." }
87+
return first()
88+
}
89+
90+
/**
91+
* The all arguments.
92+
*/
93+
fun all(): List<Any> = invocationOnMock.arguments.toList()
3994
}

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

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,25 @@
2525

2626
package org.mockito.kotlin
2727

28-
import org.mockito.kotlin.internal.createInstance
2928
import kotlinx.coroutines.runBlocking
3029
import org.mockito.Mockito
3130
import org.mockito.exceptions.misusing.NotAMockException
31+
import org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress
32+
import org.mockito.kotlin.internal.createInstance
3233
import org.mockito.stubbing.OngoingStubbing
3334
import org.mockito.stubbing.Stubber
3435
import kotlin.reflect.KClass
3536

36-
inline fun <T : Any> stubbing(
37-
mock: T,
38-
stubbing: KStubbing<T>.(T) -> Unit
39-
) {
37+
/**
38+
* Apply stubbing on the given mock. The stubbed behavior of the mock can then be specified in a supplied lambda.
39+
*/
40+
inline fun <T : Any> stubbing(mock: T, stubbing: KStubbing<T>.(T) -> Unit) {
4041
KStubbing(mock).stubbing(mock)
4142
}
4243

44+
/**
45+
* Apply stubbing on the given mock. The stubbed behavior of the mock can then be specified in a supplied lambda.
46+
*/
4347
inline fun <T : Any> T.stub(stubbing: KStubbing<T>.(T) -> Unit): T {
4448
return apply { KStubbing(this).stubbing(this) }
4549
}
@@ -49,44 +53,85 @@ class KStubbing<out T : Any>(val mock: T) {
4953
if (!mockingDetails(mock).isMock) throw NotAMockException("Stubbing target is not a mock!")
5054
}
5155

52-
fun <R> on(methodCall: R): OngoingStubbing<R> = Mockito.`when`(methodCall)
56+
/**
57+
* Enables stubbing methods. Use it when you want the mock to return particular value when particular method is called.
58+
*
59+
* Simply put: `When the x method is called then return y`
60+
*/
61+
fun <R> on(methodCall: R): OngoingStubbing<R> = mockitoWhen(methodCall)
62+
63+
/**
64+
* Enables stubbing methods. Use it when you want the mock to return particular value when particular method is called.
65+
*
66+
* Simply put: `When the x method is called then return y`
67+
*/
68+
fun <R> on(methodCall: T.() -> R): OngoingStubbing<R> {
69+
return try {
70+
mockitoWhen(mock.methodCall())
71+
} catch (e: NullPointerException) {
72+
throw MockitoKotlinException(
73+
"NullPointerException thrown when stubbing.\nThis may be due to two reasons:\n\t- The method you're trying to stub threw an NPE: look at the stack trace below;\n\t- You're trying to stub a generic method: try `onGeneric` instead.",
74+
e
75+
)
76+
}
77+
}
78+
79+
/**
80+
* Enables stubbing suspend functions. Use it when you want the mock to return particular value when particular suspend function is called.
81+
*
82+
* Simply put: `When the x suspend function is called then return y`
83+
*/
84+
fun <R> onBlocking(suspendFunctionCall: suspend T.() -> R): OngoingStubbing<R> {
85+
return runBlocking { mockitoWhen(mock.suspendFunctionCall()) }
86+
}
87+
88+
/**
89+
* Enables stubbing generic methods with return type [R]. Use it when you want the mock to return particular value when particular method is called.
90+
*
91+
* Simply put: `When the x method is called then return y of type R`
92+
*/
93+
inline fun <reified R : Any> onGeneric(noinline methodCall: T.() -> R?): OngoingStubbing<R?> {
94+
return onGeneric(methodCall, R::class)
95+
}
5396

54-
fun <R : Any> onGeneric(methodCall: T.() -> R?, c: KClass<R>): OngoingStubbing<R> {
97+
98+
/**
99+
* Enables stubbing generic methods with return type [R]. Use it when you want the mock to return particular value when particular method is called.
100+
*
101+
* Simply put: `When the x method is called then return y of type R`
102+
*/
103+
fun <R : Any> onGeneric(methodCall: T.() -> R?, c: KClass<R>): OngoingStubbing<R?> {
55104
val r = try {
56105
mock.methodCall()
57-
} catch (e: NullPointerException) {
106+
} catch (_: NullPointerException) {
58107
// An NPE may be thrown by the Kotlin type system when the MockMethodInterceptor returns a
59108
// null value for a non-nullable generic type.
60109
// We catch this NPE to return a valid instance.
61110
// The Mockito state has already been modified at this point to reflect
62111
// the wanted changes.
63112
createInstance(c)
64113
}
65-
return Mockito.`when`(r)
114+
return mockitoWhen(r)
66115
}
67116

68-
inline fun <reified R : Any> onGeneric(noinline methodCall: T.() -> R?): OngoingStubbing<R> {
69-
return onGeneric(methodCall, R::class)
117+
/**
118+
* Completes stubbing a method, by addressing the method call to apply a given stubbed [org.mockito.stubbing.Answer] on.
119+
* Use it when you want the mock to return particular value when particular method is called.
120+
*
121+
* Simply put: `Return y when the x method is called`
122+
*/
123+
fun Stubber.on(methodCall: T.() -> Unit) {
124+
this.`when`(mock).methodCall()
70125
}
71126

72-
fun <R> on(methodCall: T.() -> R): OngoingStubbing<R> {
73-
return try {
74-
Mockito.`when`(mock.methodCall())
75-
} catch (e: NullPointerException) {
76-
throw MockitoKotlinException(
77-
"NullPointerException thrown when stubbing.\nThis may be due to two reasons:\n\t- The method you're trying to stub threw an NPE: look at the stack trace below;\n\t- You're trying to stub a generic method: try `onGeneric` instead.",
78-
e
79-
)
80-
}
81-
}
127+
private fun <R> mockitoWhen(methodCall: R): OngoingStubbing<R> {
128+
val ongoingStubbing = Mockito.`when`(methodCall)
82129

83-
fun <T : Any, R> KStubbing<T>.onBlocking(
84-
m: suspend T.() -> R
85-
): OngoingStubbing<R> {
86-
return runBlocking { Mockito.`when`(mock.m()) }
87-
}
130+
if (ongoingStubbing.getMock<T>() != mock) {
131+
mockingProgress().reset()
132+
throw IllegalArgumentException("Stubbing of another mock is not allowed")
133+
}
88134

89-
fun Stubber.on(methodCall: T.() -> Unit) {
90-
this.`when`(mock).methodCall()
135+
return ongoingStubbing
91136
}
92137
}

0 commit comments

Comments
 (0)