Skip to content

Commit c663d95

Browse files
authored
Port missing kotlin specific stubbing methods to BDDMockito (#429)
1 parent 3173393 commit c663d95

File tree

5 files changed

+172
-12
lines changed

5 files changed

+172
-12
lines changed

mockito-kotlin/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +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.kotlin:kotlin-test:$kotlin_version"
3435
testCompile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
3536

3637
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0"

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ 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
33+
import kotlin.reflect.KClass
3234

3335
/**
3436
* Alias for [BDDMockito.given].
@@ -65,6 +67,13 @@ infix fun <T> BDDMyOngoingStubbing<T>.willAnswer(value: (InvocationOnMock) -> T?
6567
return willAnswer { value(it) }
6668
}
6769

70+
/**
71+
* Alias for [BBDMyOngoingStubbing.willAnswer], accepting a suspend lambda.
72+
*/
73+
infix fun <T> BDDMyOngoingStubbing<T>.willSuspendableAnswer(value: suspend (InvocationOnMock) -> T?): BDDMockito.BDDMyOngoingStubbing<T> {
74+
return willAnswer(SuspendableAnswer(value))
75+
}
76+
6877
/**
6978
* Alias for [BBDMyOngoingStubbing.willReturn].
7079
*/
@@ -79,3 +88,34 @@ infix fun <T> BDDMyOngoingStubbing<T>.willThrow(value: () -> Throwable): BDDMock
7988
return willThrow(value())
8089
}
8190

91+
/**
92+
* Sets a Throwable type to be thrown when the method is called.
93+
*
94+
* Alias for [BDDMyOngoingStubbing.willThrow]
95+
*/
96+
infix fun <T> BDDMyOngoingStubbing<T>.willThrow(t: KClass<out Throwable>): BDDMyOngoingStubbing<T> {
97+
return willThrow(t.java)
98+
}
99+
100+
/**
101+
* Sets Throwable classes to be thrown when the method is called.
102+
*
103+
* Alias for [BDDMyOngoingStubbing.willThrow]
104+
*/
105+
fun <T> BDDMyOngoingStubbing<T>.willThrow(
106+
t: KClass<out Throwable>,
107+
vararg ts: KClass<out Throwable>
108+
): BDDMyOngoingStubbing<T> {
109+
return willThrow(t.java, *ts.map { it.java }.toTypedArray())
110+
}
111+
112+
/**
113+
* Sets consecutive return values to be returned when the method is called.
114+
* Same as [BDDMyOngoingStubbing.willReturn], but accepts list instead of varargs.
115+
*/
116+
inline infix fun <reified T> BDDMyOngoingStubbing<T>.willReturnConsecutively(ts: List<T>): BDDMyOngoingStubbing<T> {
117+
return willReturn(
118+
ts[0],
119+
*ts.drop(1).toTypedArray()
120+
)
121+
}

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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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.Assert.assertEquals
7+
import org.junit.Test
8+
import kotlin.test.assertFailsWith
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+
@Test
39+
fun willThrow_kclass_single() {
40+
val fixture: SomeInterface = mock()
41+
42+
given(fixture.foo()).willThrow(RuntimeException::class)
43+
44+
assertFailsWith(RuntimeException::class) {
45+
fixture.foo()
46+
}
47+
}
48+
49+
@Test
50+
fun willThrow_kclass_multiple() {
51+
val fixture: SomeInterface = mock()
52+
53+
given(fixture.foo()).willThrow(RuntimeException::class, IllegalArgumentException::class)
54+
55+
assertFailsWith(RuntimeException::class) {
56+
fixture.foo()
57+
}
58+
assertFailsWith(IllegalArgumentException::class) {
59+
fixture.foo()
60+
}
61+
}
62+
63+
@Test
64+
fun willReturnConsecutively() {
65+
val fixture: SomeInterface = mock()
66+
67+
given(fixture.foo()).willReturnConsecutively(listOf(42, 24))
68+
69+
assertEquals(42, fixture.foo())
70+
assertEquals(24, fixture.foo())
71+
}
72+
}
73+
74+
interface SomeInterface {
75+
fun foo(): Int
76+
77+
suspend fun suspending(): Int
78+
suspend fun suspendingWithArg(arg: Int): Int
79+
}

0 commit comments

Comments
 (0)