Skip to content

Commit d8fe7c7

Browse files
authored
Add value class support to KArgumentCaptor (#547)
1 parent 02f33bc commit d8fe7c7

File tree

5 files changed

+106
-34
lines changed

5 files changed

+106
-34
lines changed

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

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import kotlin.reflect.KClass
3434
* Creates a [KArgumentCaptor] for given type.
3535
*/
3636
inline fun <reified T : Any> argumentCaptor(): KArgumentCaptor<T> {
37-
return KArgumentCaptor(ArgumentCaptor.forClass(T::class.java), T::class)
37+
return KArgumentCaptor(T::class)
3838
}
3939

4040
/**
@@ -45,8 +45,8 @@ inline fun <reified A : Any, reified B : Any> argumentCaptor(
4545
b: KClass<B> = B::class
4646
): Pair<KArgumentCaptor<A>, KArgumentCaptor<B>> {
4747
return Pair(
48-
KArgumentCaptor(ArgumentCaptor.forClass(a.java), a),
49-
KArgumentCaptor(ArgumentCaptor.forClass(b.java), b)
48+
KArgumentCaptor(a),
49+
KArgumentCaptor(b)
5050
)
5151
}
5252

@@ -59,9 +59,9 @@ inline fun <reified A : Any, reified B : Any, reified C : Any> argumentCaptor(
5959
c: KClass<C> = C::class
6060
): Triple<KArgumentCaptor<A>, KArgumentCaptor<B>, KArgumentCaptor<C>> {
6161
return Triple(
62-
KArgumentCaptor(ArgumentCaptor.forClass(a.java), a),
63-
KArgumentCaptor(ArgumentCaptor.forClass(b.java), b),
64-
KArgumentCaptor(ArgumentCaptor.forClass(c.java), c)
62+
KArgumentCaptor(a),
63+
KArgumentCaptor(b),
64+
KArgumentCaptor(c)
6565
)
6666
}
6767

@@ -103,10 +103,10 @@ inline fun <reified A : Any, reified B : Any, reified C : Any, reified D : Any>
103103
d: KClass<D> = D::class
104104
): ArgumentCaptorHolder4<KArgumentCaptor<A>, KArgumentCaptor<B>, KArgumentCaptor<C>, KArgumentCaptor<D>> {
105105
return ArgumentCaptorHolder4(
106-
KArgumentCaptor(ArgumentCaptor.forClass(a.java), a),
107-
KArgumentCaptor(ArgumentCaptor.forClass(b.java), b),
108-
KArgumentCaptor(ArgumentCaptor.forClass(c.java), c),
109-
KArgumentCaptor(ArgumentCaptor.forClass(d.java), d)
106+
KArgumentCaptor(a),
107+
KArgumentCaptor(b),
108+
KArgumentCaptor(c),
109+
KArgumentCaptor(d)
110110
)
111111
}
112112

@@ -121,11 +121,11 @@ inline fun <reified A : Any, reified B : Any, reified C : Any, reified D : Any,
121121
e: KClass<E> = E::class
122122
): ArgumentCaptorHolder5<KArgumentCaptor<A>, KArgumentCaptor<B>, KArgumentCaptor<C>, KArgumentCaptor<D>, KArgumentCaptor<E>> {
123123
return ArgumentCaptorHolder5(
124-
KArgumentCaptor(ArgumentCaptor.forClass(a.java), a),
125-
KArgumentCaptor(ArgumentCaptor.forClass(b.java), b),
126-
KArgumentCaptor(ArgumentCaptor.forClass(c.java), c),
127-
KArgumentCaptor(ArgumentCaptor.forClass(d.java), d),
128-
KArgumentCaptor(ArgumentCaptor.forClass(e.java), e)
124+
KArgumentCaptor(a),
125+
KArgumentCaptor(b),
126+
KArgumentCaptor(c),
127+
KArgumentCaptor(d),
128+
KArgumentCaptor(e)
129129
)
130130
}
131131

@@ -140,7 +140,7 @@ inline fun <reified T : Any> argumentCaptor(f: KArgumentCaptor<T>.() -> Unit): K
140140
* Creates a [KArgumentCaptor] for given nullable type.
141141
*/
142142
inline fun <reified T : Any> nullableArgumentCaptor(): KArgumentCaptor<T?> {
143-
return KArgumentCaptor(ArgumentCaptor.forClass(T::class.java), T::class)
143+
return KArgumentCaptor(T::class)
144144
}
145145

146146
/**
@@ -157,48 +157,58 @@ inline fun <reified T : Any> capture(captor: ArgumentCaptor<T>): T {
157157
return captor.capture() ?: createInstance()
158158
}
159159

160-
class KArgumentCaptor<out T : Any?>(
161-
private val captor: ArgumentCaptor<T>,
160+
class KArgumentCaptor<out T : Any?> (
162161
private val tClass: KClass<*>
163162
) {
163+
private val captor: ArgumentCaptor<Any?> =
164+
if (tClass.isValue) {
165+
val boxImpl =
166+
tClass.java.declaredMethods
167+
.single { it.name == "box-impl" && it.parameterCount == 1 }
168+
boxImpl.parameters[0].type // is the boxed type of the value type
169+
} else {
170+
tClass.java
171+
}.let {
172+
ArgumentCaptor.forClass(it)
173+
}
164174

165175
/**
166176
* The first captured value of the argument.
167177
* @throws IndexOutOfBoundsException if the value is not available.
168178
*/
169179
val firstValue: T
170-
get() = captor.firstValue
180+
get() = toKotlinType(captor.firstValue)
171181

172182
/**
173183
* The second captured value of the argument.
174184
* @throws IndexOutOfBoundsException if the value is not available.
175185
*/
176186
val secondValue: T
177-
get() = captor.secondValue
187+
get() = toKotlinType(captor.secondValue)
178188

179189
/**
180190
* The third captured value of the argument.
181191
* @throws IndexOutOfBoundsException if the value is not available.
182192
*/
183193
val thirdValue: T
184-
get() = captor.thirdValue
194+
get() = toKotlinType(captor.thirdValue)
185195

186196
/**
187197
* The last captured value of the argument.
188198
* @throws IndexOutOfBoundsException if the value is not available.
189199
*/
190200
val lastValue: T
191-
get() = captor.lastValue
201+
get() = toKotlinType(captor.lastValue)
192202

193203
/**
194204
* The *only* captured value of the argument,
195205
* or throws an exception if no value or more than one value was captured.
196206
*/
197207
val singleValue: T
198-
get() = captor.singleValue
208+
get() = toKotlinType(captor.singleValue)
199209

200210
val allValues: List<T>
201-
get() = captor.allValues
211+
get() = captor.allValues.map(::toKotlinType)
202212

203213
@Suppress("UNCHECKED_CAST")
204214
fun capture(): T {
@@ -209,14 +219,28 @@ class KArgumentCaptor<out T : Any?>(
209219
// In Java, `captor.capture` returns null and so the method is called with `[null]`
210220
// In Kotlin, we have to create `[null]` explicitly.
211221
// This code-path is applied for non-vararg array arguments as well, but it seems to work fine.
212-
return captor.capture() ?: if (tClass.java.isArray) {
222+
return captor.capture() as T ?: if (tClass.java.isArray) {
213223
singleElementArray()
214224
} else {
215225
createInstance(tClass)
216226
} as T
217227
}
218228

219229
private fun singleElementArray(): Any? = Array.newInstance(tClass.java.componentType, 1)
230+
231+
@Suppress("UNCHECKED_CAST")
232+
private fun toKotlinType(rawCapturedValue: Any?) : T {
233+
return if(tClass.isValue) {
234+
rawCapturedValue
235+
?.let {
236+
val boxImpl =
237+
tClass.java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 }
238+
boxImpl.invoke(null, it)
239+
} as T
240+
} else {
241+
rawCapturedValue as T
242+
}
243+
}
220244
}
221245

222246
val <T> ArgumentCaptor<T>.firstValue: T

mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/CreateInstance.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,15 @@ inline fun <reified T : Any> createInstance(): T {
4141
}
4242
}
4343

44+
@Suppress("UNCHECKED_CAST")
4445
fun <T : Any> createInstance(@Suppress("UNUSED_PARAMETER") kClass: KClass<T>): T {
45-
return castNull()
46+
return if(kClass.isValue) {
47+
val boxImpl =
48+
kClass.java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 }
49+
boxImpl.invoke(null, castNull()) as T
50+
} else {
51+
castNull()
52+
}
4653
}
4754

4855
/**

tests/src/test/kotlin/test/ArgumentCaptorTest.kt

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ package test
33
import com.nhaarman.expect.expect
44
import com.nhaarman.expect.expectErrorWithMessage
55
import org.junit.Test
6-
import org.mockito.ArgumentCaptor
7-
import org.mockito.ArgumentMatchers
8-
import org.mockito.ArgumentMatchers.anyLong
96
import org.mockito.kotlin.*
107
import java.util.*
118

@@ -342,4 +339,47 @@ class ArgumentCaptorTest : TestBase() {
342339
verify(m).stringArray(captor.capture())
343340
expect(captor.firstValue.toList()).toBe(listOf())
344341
}
342+
343+
@Test
344+
fun argumentCaptor_value_class() {
345+
/* Given */
346+
val m: Methods = mock()
347+
val valueClass = ValueClass("Content")
348+
349+
/* When */
350+
m.valueClass(valueClass)
351+
352+
/* Then */
353+
val captor = argumentCaptor<ValueClass>()
354+
verify(m).valueClass(captor.capture())
355+
expect(captor.firstValue).toBe(valueClass)
356+
}
357+
358+
@Test
359+
fun argumentCaptor_value_class_withNullValue_usingNonNullable() {
360+
/* Given */
361+
val m: Methods = mock()
362+
363+
/* When */
364+
m.nullableValueClass(null)
365+
366+
/* Then */
367+
val captor = argumentCaptor<ValueClass>()
368+
verify(m).nullableValueClass(captor.capture())
369+
expect(captor.firstValue).toBeNull()
370+
}
371+
372+
@Test
373+
fun argumentCaptor_value_class_withNullValue_usingNullable() {
374+
/* Given */
375+
val m: Methods = mock()
376+
377+
/* When */
378+
m.nullableValueClass(null)
379+
380+
/* Then */
381+
val captor = nullableArgumentCaptor<ValueClass>()
382+
verify(m).nullableValueClass(captor.capture())
383+
expect(captor.firstValue).toBeNull()
384+
}
345385
}

tests/src/test/kotlin/test/Classes.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ interface Methods {
8686

8787
fun nonDefaultReturnType(): ExtraInterface
8888

89-
fun valueClass(v: ValueClass?)
89+
fun valueClass(v: ValueClass)
90+
fun nullableValueClass(v: ValueClass?)
9091
fun nestedValueClass(v: NestedValueClass)
9192
}
9293

tests/src/test/kotlin/test/MatchersTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,16 +349,16 @@ class MatchersTest : TestBase() {
349349
@Test
350350
fun anyOrNull_forValueClass() {
351351
mock<Methods>().apply {
352-
valueClass(ValueClass("Content"))
353-
verify(this).valueClass(anyOrNull())
352+
nullableValueClass(ValueClass("Content"))
353+
verify(this).nullableValueClass(anyOrNull())
354354
}
355355
}
356356

357357
@Test
358358
fun anyOrNull_forValueClass_withNull() {
359359
mock<Methods>().apply {
360-
valueClass(null)
361-
verify(this).valueClass(anyOrNull())
360+
nullableValueClass(null)
361+
verify(this).nullableValueClass(anyOrNull())
362362
}
363363
}
364364

0 commit comments

Comments
 (0)