From 370288d2b10ec0776dc78c1f2436f30a2a70e7ae Mon Sep 17 00:00:00 2001 From: Mark Koops Date: Tue, 21 Oct 2025 22:14:58 +0200 Subject: [PATCH] Adding value class support to KArgumentCaptor. --- .../org/mockito/kotlin/ArgumentCaptor.kt | 74 ++++++++++++------- .../mockito/kotlin/internal/CreateInstance.kt | 9 ++- .../test/kotlin/test/ArgumentCaptorTest.kt | 46 +++++++++++- tests/src/test/kotlin/test/Classes.kt | 3 +- tests/src/test/kotlin/test/MatchersTest.kt | 8 +- 5 files changed, 106 insertions(+), 34 deletions(-) diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt index d10a650b..89fbdb95 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt @@ -34,7 +34,7 @@ import kotlin.reflect.KClass * Creates a [KArgumentCaptor] for given type. */ inline fun argumentCaptor(): KArgumentCaptor { - return KArgumentCaptor(ArgumentCaptor.forClass(T::class.java), T::class) + return KArgumentCaptor(T::class) } /** @@ -45,8 +45,8 @@ inline fun argumentCaptor( b: KClass = B::class ): Pair, KArgumentCaptor> { return Pair( - KArgumentCaptor(ArgumentCaptor.forClass(a.java), a), - KArgumentCaptor(ArgumentCaptor.forClass(b.java), b) + KArgumentCaptor(a), + KArgumentCaptor(b) ) } @@ -59,9 +59,9 @@ inline fun argumentCaptor( c: KClass = C::class ): Triple, KArgumentCaptor, KArgumentCaptor> { return Triple( - KArgumentCaptor(ArgumentCaptor.forClass(a.java), a), - KArgumentCaptor(ArgumentCaptor.forClass(b.java), b), - KArgumentCaptor(ArgumentCaptor.forClass(c.java), c) + KArgumentCaptor(a), + KArgumentCaptor(b), + KArgumentCaptor(c) ) } @@ -103,10 +103,10 @@ inline fun d: KClass = D::class ): ArgumentCaptorHolder4, KArgumentCaptor, KArgumentCaptor, KArgumentCaptor> { return ArgumentCaptorHolder4( - KArgumentCaptor(ArgumentCaptor.forClass(a.java), a), - KArgumentCaptor(ArgumentCaptor.forClass(b.java), b), - KArgumentCaptor(ArgumentCaptor.forClass(c.java), c), - KArgumentCaptor(ArgumentCaptor.forClass(d.java), d) + KArgumentCaptor(a), + KArgumentCaptor(b), + KArgumentCaptor(c), + KArgumentCaptor(d) ) } @@ -121,11 +121,11 @@ inline fun = E::class ): ArgumentCaptorHolder5, KArgumentCaptor, KArgumentCaptor, KArgumentCaptor, KArgumentCaptor> { return ArgumentCaptorHolder5( - KArgumentCaptor(ArgumentCaptor.forClass(a.java), a), - KArgumentCaptor(ArgumentCaptor.forClass(b.java), b), - KArgumentCaptor(ArgumentCaptor.forClass(c.java), c), - KArgumentCaptor(ArgumentCaptor.forClass(d.java), d), - KArgumentCaptor(ArgumentCaptor.forClass(e.java), e) + KArgumentCaptor(a), + KArgumentCaptor(b), + KArgumentCaptor(c), + KArgumentCaptor(d), + KArgumentCaptor(e) ) } @@ -140,7 +140,7 @@ inline fun argumentCaptor(f: KArgumentCaptor.() -> Unit): K * Creates a [KArgumentCaptor] for given nullable type. */ inline fun nullableArgumentCaptor(): KArgumentCaptor { - return KArgumentCaptor(ArgumentCaptor.forClass(T::class.java), T::class) + return KArgumentCaptor(T::class) } /** @@ -157,48 +157,58 @@ inline fun capture(captor: ArgumentCaptor): T { return captor.capture() ?: createInstance() } -class KArgumentCaptor( - private val captor: ArgumentCaptor, +class KArgumentCaptor ( private val tClass: KClass<*> ) { + private val captor: ArgumentCaptor = + if (tClass.isValue) { + val boxImpl = + tClass.java.declaredMethods + .single { it.name == "box-impl" && it.parameterCount == 1 } + boxImpl.parameters[0].type // is the boxed type of the value type + } else { + tClass.java + }.let { + ArgumentCaptor.forClass(it) + } /** * The first captured value of the argument. * @throws IndexOutOfBoundsException if the value is not available. */ val firstValue: T - get() = captor.firstValue + get() = toKotlinType(captor.firstValue) /** * The second captured value of the argument. * @throws IndexOutOfBoundsException if the value is not available. */ val secondValue: T - get() = captor.secondValue + get() = toKotlinType(captor.secondValue) /** * The third captured value of the argument. * @throws IndexOutOfBoundsException if the value is not available. */ val thirdValue: T - get() = captor.thirdValue + get() = toKotlinType(captor.thirdValue) /** * The last captured value of the argument. * @throws IndexOutOfBoundsException if the value is not available. */ val lastValue: T - get() = captor.lastValue + get() = toKotlinType(captor.lastValue) /** * The *only* captured value of the argument, * or throws an exception if no value or more than one value was captured. */ val singleValue: T - get() = captor.singleValue + get() = toKotlinType(captor.singleValue) val allValues: List - get() = captor.allValues + get() = captor.allValues.map(::toKotlinType) @Suppress("UNCHECKED_CAST") fun capture(): T { @@ -209,7 +219,7 @@ class KArgumentCaptor( // In Java, `captor.capture` returns null and so the method is called with `[null]` // In Kotlin, we have to create `[null]` explicitly. // This code-path is applied for non-vararg array arguments as well, but it seems to work fine. - return captor.capture() ?: if (tClass.java.isArray) { + return captor.capture() as T ?: if (tClass.java.isArray) { singleElementArray() } else { createInstance(tClass) @@ -217,6 +227,20 @@ class KArgumentCaptor( } private fun singleElementArray(): Any? = Array.newInstance(tClass.java.componentType, 1) + + @Suppress("UNCHECKED_CAST") + private fun toKotlinType(rawCapturedValue: Any?) : T { + return if(tClass.isValue) { + rawCapturedValue + ?.let { + val boxImpl = + tClass.java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 } + boxImpl.invoke(null, it) + } as T + } else { + rawCapturedValue as T + } + } } val ArgumentCaptor.firstValue: T diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/CreateInstance.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/CreateInstance.kt index f2a85a50..0515e1b1 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/CreateInstance.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/CreateInstance.kt @@ -41,8 +41,15 @@ inline fun createInstance(): T { } } +@Suppress("UNCHECKED_CAST") fun createInstance(@Suppress("UNUSED_PARAMETER") kClass: KClass): T { - return castNull() + return if(kClass.isValue) { + val boxImpl = + kClass.java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 } + boxImpl.invoke(null, castNull()) as T + } else { + castNull() + } } /** diff --git a/tests/src/test/kotlin/test/ArgumentCaptorTest.kt b/tests/src/test/kotlin/test/ArgumentCaptorTest.kt index 323d7a45..14382a5c 100644 --- a/tests/src/test/kotlin/test/ArgumentCaptorTest.kt +++ b/tests/src/test/kotlin/test/ArgumentCaptorTest.kt @@ -3,9 +3,6 @@ package test import com.nhaarman.expect.expect import com.nhaarman.expect.expectErrorWithMessage import org.junit.Test -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers -import org.mockito.ArgumentMatchers.anyLong import org.mockito.kotlin.* import java.util.* @@ -342,4 +339,47 @@ class ArgumentCaptorTest : TestBase() { verify(m).stringArray(captor.capture()) expect(captor.firstValue.toList()).toBe(listOf()) } + + @Test + fun argumentCaptor_value_class() { + /* Given */ + val m: Methods = mock() + val valueClass = ValueClass("Content") + + /* When */ + m.valueClass(valueClass) + + /* Then */ + val captor = argumentCaptor() + verify(m).valueClass(captor.capture()) + expect(captor.firstValue).toBe(valueClass) + } + + @Test + fun argumentCaptor_value_class_withNullValue_usingNonNullable() { + /* Given */ + val m: Methods = mock() + + /* When */ + m.nullableValueClass(null) + + /* Then */ + val captor = argumentCaptor() + verify(m).nullableValueClass(captor.capture()) + expect(captor.firstValue).toBeNull() + } + + @Test + fun argumentCaptor_value_class_withNullValue_usingNullable() { + /* Given */ + val m: Methods = mock() + + /* When */ + m.nullableValueClass(null) + + /* Then */ + val captor = nullableArgumentCaptor() + verify(m).nullableValueClass(captor.capture()) + expect(captor.firstValue).toBeNull() + } } diff --git a/tests/src/test/kotlin/test/Classes.kt b/tests/src/test/kotlin/test/Classes.kt index 0c20200e..a295f388 100644 --- a/tests/src/test/kotlin/test/Classes.kt +++ b/tests/src/test/kotlin/test/Classes.kt @@ -86,7 +86,8 @@ interface Methods { fun nonDefaultReturnType(): ExtraInterface - fun valueClass(v: ValueClass?) + fun valueClass(v: ValueClass) + fun nullableValueClass(v: ValueClass?) fun nestedValueClass(v: NestedValueClass) } diff --git a/tests/src/test/kotlin/test/MatchersTest.kt b/tests/src/test/kotlin/test/MatchersTest.kt index 341234a0..4088d183 100644 --- a/tests/src/test/kotlin/test/MatchersTest.kt +++ b/tests/src/test/kotlin/test/MatchersTest.kt @@ -349,16 +349,16 @@ class MatchersTest : TestBase() { @Test fun anyOrNull_forValueClass() { mock().apply { - valueClass(ValueClass("Content")) - verify(this).valueClass(anyOrNull()) + nullableValueClass(ValueClass("Content")) + verify(this).nullableValueClass(anyOrNull()) } } @Test fun anyOrNull_forValueClass_withNull() { mock().apply { - valueClass(null) - verify(this).valueClass(anyOrNull()) + nullableValueClass(null) + verify(this).nullableValueClass(anyOrNull()) } }