From f6023c03a2b84cdc5524ab2a158184dcdee874a3 Mon Sep 17 00:00:00 2001 From: Mark Koops Date: Wed, 10 Dec 2025 20:46:07 +0100 Subject: [PATCH 1/4] Fix handling of primitive value class arguments in eq matcher and argumentCaptor. --- .../org/mockito/kotlin/ArgumentCaptor.kt | 100 +++++++++--------- .../kotlin/org/mockito/kotlin/Matchers.kt | 9 +- .../test/kotlin/test/ArgumentCaptorTest.kt | 15 ++- tests/src/test/kotlin/test/Classes.kt | 8 +- .../test/CoroutinesOngoingStubbingTest.kt | 36 +++++++ tests/src/test/kotlin/test/MatchersTest.kt | 17 ++- .../test/kotlin/test/OngoingStubbingTest.kt | 30 ++++++ 7 files changed, 154 insertions(+), 61 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 89fbdb9..e1f036c 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt @@ -25,28 +25,30 @@ package org.mockito.kotlin -import org.mockito.kotlin.internal.createInstance import org.mockito.ArgumentCaptor +import org.mockito.kotlin.internal.createInstance import java.lang.reflect.Array import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.typeOf /** * Creates a [KArgumentCaptor] for given type. */ -inline fun argumentCaptor(): KArgumentCaptor { - return KArgumentCaptor(T::class) +inline fun argumentCaptor(): KArgumentCaptor { + return KArgumentCaptor(typeOf()) } /** * Creates 2 [KArgumentCaptor]s for given types. */ inline fun argumentCaptor( - a: KClass = A::class, - b: KClass = B::class + @Suppress("unused") a: KClass = A::class, + @Suppress("unused") b: KClass = B::class ): Pair, KArgumentCaptor> { return Pair( - KArgumentCaptor(a), - KArgumentCaptor(b) + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()) ) } @@ -54,14 +56,14 @@ inline fun argumentCaptor( * Creates 3 [KArgumentCaptor]s for given types. */ inline fun argumentCaptor( - a: KClass = A::class, - b: KClass = B::class, - c: KClass = C::class + @Suppress("unused") a: KClass = A::class, + @Suppress("unused") b: KClass = B::class, + @Suppress("unused") c: KClass = C::class ): Triple, KArgumentCaptor, KArgumentCaptor> { return Triple( - KArgumentCaptor(a), - KArgumentCaptor(b), - KArgumentCaptor(c) + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()) ) } @@ -97,16 +99,16 @@ class ArgumentCaptorHolder5( * Creates 4 [KArgumentCaptor]s for given types. */ inline fun argumentCaptor( - a: KClass = A::class, - b: KClass = B::class, - c: KClass = C::class, - d: KClass = D::class + @Suppress("unused") a: KClass = A::class, + @Suppress("unused") b: KClass = B::class, + @Suppress("unused") c: KClass = C::class, + @Suppress("unused") d: KClass = D::class ): ArgumentCaptorHolder4, KArgumentCaptor, KArgumentCaptor, KArgumentCaptor> { return ArgumentCaptorHolder4( - KArgumentCaptor(a), - KArgumentCaptor(b), - KArgumentCaptor(c), - KArgumentCaptor(d) + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()) ) } @@ -114,18 +116,18 @@ inline fun * Creates 4 [KArgumentCaptor]s for given types. */ inline fun argumentCaptor( - a: KClass = A::class, - b: KClass = B::class, - c: KClass = C::class, - d: KClass = D::class, - e: KClass = E::class + @Suppress("unused") a: KClass = A::class, + @Suppress("unused") b: KClass = B::class, + @Suppress("unused") c: KClass = C::class, + @Suppress("unused") d: KClass = D::class, + @Suppress("unused") e: KClass = E::class ): ArgumentCaptorHolder5, KArgumentCaptor, KArgumentCaptor, KArgumentCaptor, KArgumentCaptor> { return ArgumentCaptorHolder5( - KArgumentCaptor(a), - KArgumentCaptor(b), - KArgumentCaptor(c), - KArgumentCaptor(d), - KArgumentCaptor(e) + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()) ) } @@ -140,7 +142,7 @@ inline fun argumentCaptor(f: KArgumentCaptor.() -> Unit): K * Creates a [KArgumentCaptor] for given nullable type. */ inline fun nullableArgumentCaptor(): KArgumentCaptor { - return KArgumentCaptor(T::class) + return KArgumentCaptor(typeOf()) } /** @@ -157,17 +159,17 @@ inline fun capture(captor: ArgumentCaptor): T { return captor.capture() ?: createInstance() } -class KArgumentCaptor ( - private val tClass: KClass<*> -) { +class KArgumentCaptor(private val kType: KType) { + private val clazz = kType.classifier as KClass<*> + private val captor: ArgumentCaptor = - if (tClass.isValue) { + if (clazz.isValue && !kType.isMarkedNullable) { val boxImpl = - tClass.java.declaredMethods + clazz.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 + clazz.java }.let { ArgumentCaptor.forClass(it) } @@ -219,27 +221,29 @@ 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() as T ?: if (tClass.java.isArray) { + return toKotlinType(captor.capture()) ?: if (clazz.java.isArray) { singleElementArray() } else { - createInstance(tClass) + createInstance(clazz) } as T } - private fun singleElementArray(): Any? = Array.newInstance(tClass.java.componentType, 1) + private fun singleElementArray(): Any? = Array.newInstance(clazz.java.componentType, 1) @Suppress("UNCHECKED_CAST") - private fun toKotlinType(rawCapturedValue: Any?) : T { - return if(tClass.isValue) { - rawCapturedValue - ?.let { + private fun toKotlinType(rawCapturedValue: Any?): T { + if (rawCapturedValue == null) return null as T + + if (clazz.isValue && rawCapturedValue::class != clazz) { + return rawCapturedValue + .let { val boxImpl = - tClass.java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 } + clazz.java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 } boxImpl.invoke(null, it) } as T - } else { - rawCapturedValue as T } + + return rawCapturedValue as T } } diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt index e276c41..39c69a4 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt @@ -29,6 +29,7 @@ import org.mockito.ArgumentMatcher import org.mockito.ArgumentMatchers import org.mockito.kotlin.internal.createInstance import kotlin.reflect.KClass +import kotlin.reflect.typeOf /** Object argument that is equal to the given value. */ inline fun eq(value: T): T { @@ -91,11 +92,17 @@ inline fun anyValueClass(): T { return boxImpl.invoke(null, ArgumentMatchers.any(boxedType)) as T } -inline fun eqValueClass(value: T): T { +inline fun eqValueClass(value: T): T { require(T::class.isValue) { "${T::class.qualifiedName} is not a value class." } + if (typeOf().isMarkedNullable) { + // if the value is both value class and nullable, then Kotlin passes the value class boxed + // towards Mockito java code. + return ArgumentMatchers.eq(value) + } + val unboxImpl = T::class.java.declaredMethods .single { it.name == "unbox-impl" && it.parameterCount == 0 } diff --git a/tests/src/test/kotlin/test/ArgumentCaptorTest.kt b/tests/src/test/kotlin/test/ArgumentCaptorTest.kt index d467088..d1025f2 100644 --- a/tests/src/test/kotlin/test/ArgumentCaptorTest.kt +++ b/tests/src/test/kotlin/test/ArgumentCaptorTest.kt @@ -2,9 +2,16 @@ package test import com.nhaarman.expect.expect import com.nhaarman.expect.expectErrorWithMessage -import org.junit.Ignore import org.junit.Test -import org.mockito.kotlin.* +import org.mockito.kotlin.KArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.mock +import org.mockito.kotlin.nullableArgumentCaptor +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import java.util.* class ArgumentCaptorTest : TestBase() { @@ -385,7 +392,6 @@ class ArgumentCaptorTest : TestBase() { } @Test - @Ignore("See issue #555") fun argumentCaptor_primitive_value_class() { /* Given */ val m: SynchronousFunctions = mock() @@ -401,7 +407,6 @@ class ArgumentCaptorTest : TestBase() { } @Test - @Ignore("See issue #555") fun argumentCaptor_nullable_primitive_value_class() { /* Given */ val m: SynchronousFunctions = mock() @@ -411,7 +416,7 @@ class ArgumentCaptorTest : TestBase() { m.nullablePrimitiveValueClass(valueClass) /* Then */ - val captor = argumentCaptor() + val captor = argumentCaptor() verify(m).nullablePrimitiveValueClass(captor.capture()) expect(captor.firstValue).toBe(valueClass) } diff --git a/tests/src/test/kotlin/test/Classes.kt b/tests/src/test/kotlin/test/Classes.kt index b93e2d2..a7fdba8 100644 --- a/tests/src/test/kotlin/test/Classes.kt +++ b/tests/src/test/kotlin/test/Classes.kt @@ -98,11 +98,13 @@ interface SynchronousFunctions { fun valueClass(v: ValueClass) fun nullableValueClass(v: ValueClass?) fun nestedValueClass(v: NestedValueClass) + fun primitiveValueClass(v: PrimitiveValueClass) + fun nullablePrimitiveValueClass(v: PrimitiveValueClass?) fun valueClassResult(): ValueClass fun nullableValueClassResult(): ValueClass? fun nestedValueClassResult(): NestedValueClass - fun primitiveValueClass(v: PrimitiveValueClass) - fun nullablePrimitiveValueClass(v: PrimitiveValueClass?) + fun primitiveValueClassResult(): PrimitiveValueClass + fun nullablePrimitiveValueClassResult(): PrimitiveValueClass? } interface SuspendFunctions { @@ -118,6 +120,8 @@ interface SuspendFunctions { suspend fun valueClassResult(): ValueClass suspend fun nullableValueClassResult(): ValueClass? suspend fun nestedValueClassResult(): NestedValueClass + suspend fun primitiveValueClassResult(): PrimitiveValueClass + suspend fun nullablePrimitiveValueClassResult(): PrimitiveValueClass? suspend fun builderMethod(): SuspendFunctions } diff --git a/tests/src/test/kotlin/test/CoroutinesOngoingStubbingTest.kt b/tests/src/test/kotlin/test/CoroutinesOngoingStubbingTest.kt index f3ee306..244597c 100644 --- a/tests/src/test/kotlin/test/CoroutinesOngoingStubbingTest.kt +++ b/tests/src/test/kotlin/test/CoroutinesOngoingStubbingTest.kt @@ -337,4 +337,40 @@ class CoroutinesOngoingStubbingTest { expect(result).toBe(nestedValueClass) expect(result.value).toBe(nestedValueClass.value) } + + @Test + fun `should stub suspendable function call with primitive value class result`() = runTest { + /* Given */ + val primitiveValueClass = PrimitiveValueClass(42) + val mock = mock { + on(mock.primitiveValueClassResult()) doSuspendableAnswer { + delay(1) + primitiveValueClass + } + } + + /* When */ + val result: PrimitiveValueClass = mock.primitiveValueClassResult() + + /* Then */ + expect(result).toBe(primitiveValueClass) + } + + @Test + fun `should stub suspendable function call with nullable primitive value class result`() = runTest { + /* Given */ + val primitiveValueClass = PrimitiveValueClass(42) + val mock = mock { + on (mock.nullablePrimitiveValueClassResult()) doSuspendableAnswer { + delay(1) + primitiveValueClass + } + } + + /* When */ + val result: PrimitiveValueClass? = mock.nullablePrimitiveValueClassResult() + + /* Then */ + expect(result).toBe(primitiveValueClass) + } } diff --git a/tests/src/test/kotlin/test/MatchersTest.kt b/tests/src/test/kotlin/test/MatchersTest.kt index 24ba28d..3b133cf 100644 --- a/tests/src/test/kotlin/test/MatchersTest.kt +++ b/tests/src/test/kotlin/test/MatchersTest.kt @@ -3,7 +3,6 @@ package test import com.nhaarman.expect.expect import com.nhaarman.expect.expectErrorWithMessage import kotlinx.coroutines.test.runTest -import org.junit.Ignore import org.junit.Test import org.junit.experimental.runners.Enclosed import org.junit.runner.RunWith @@ -629,12 +628,20 @@ class MatchersTest : TestBase() { } @Test - @Ignore("See issue #555") + fun eqPrimitiveValueClass() { + val primitiveValueClass = PrimitiveValueClass(123) + mock().apply { + primitiveValueClass(primitiveValueClass) + verify(this).primitiveValueClass(eq(primitiveValueClass)) + } + } + + @Test fun eqNullablePrimitiveValueClass() { - val valueClass = PrimitiveValueClass(123) + val primitiveValueClass = PrimitiveValueClass(123) as PrimitiveValueClass? mock().apply { - nullablePrimitiveValueClass(valueClass) - verify(this).nullablePrimitiveValueClass(eq(valueClass)) + nullablePrimitiveValueClass(primitiveValueClass) + verify(this).nullablePrimitiveValueClass(eq(primitiveValueClass)) } } diff --git a/tests/src/test/kotlin/test/OngoingStubbingTest.kt b/tests/src/test/kotlin/test/OngoingStubbingTest.kt index 0732fcc..0e980f4 100644 --- a/tests/src/test/kotlin/test/OngoingStubbingTest.kt +++ b/tests/src/test/kotlin/test/OngoingStubbingTest.kt @@ -317,6 +317,36 @@ class OngoingStubbingTest : TestBase() { expect(result.value).toBe(nestedValueClass.value) } + @Test + fun `should stub function call with primitive value class result`() { + /* Given */ + val primitiveValueClass = PrimitiveValueClass(42) + val mock = mock { + on { primitiveValueClassResult() } doReturn primitiveValueClass + } + + /* When */ + val result: PrimitiveValueClass = mock.primitiveValueClassResult() + + /* Then */ + expect(result).toBe(primitiveValueClass) + } + + @Test + fun `should stub function call with nullable primitive value class result`() { + /* Given */ + val primitiveValueClass = PrimitiveValueClass(42) + val mock = mock { + on { nullablePrimitiveValueClassResult() } doReturn primitiveValueClass + } + + /* When */ + val result: PrimitiveValueClass? = mock.nullablePrimitiveValueClassResult() + + /* Then */ + expect(result).toBe(primitiveValueClass) + } + @Test fun `should stub consecutive function calls with value class results`() { /* Given */ From bb0c67ad08a09f8668fb976c3858c59d1d0bf073 Mon Sep 17 00:00:00 2001 From: Mark Koops Date: Fri, 12 Dec 2025 11:09:11 +0100 Subject: [PATCH 2/4] Extract all value class handling (type deduction, boxing, unboxing, etc.) to ValueClassSupport.kt --- .../org/mockito/kotlin/ArgumentCaptor.kt | 41 ++-- .../kotlin/org/mockito/kotlin/Matchers.kt | 39 +--- .../mockito/kotlin/internal/CreateInstance.kt | 17 +- .../kotlin/internal/ValueClassSupport.kt | 81 +++++++ .../kotlin/internal/ValueClassSupportTest.kt | 202 ++++++++++++++++++ 5 files changed, 306 insertions(+), 74 deletions(-) create mode 100644 mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/ValueClassSupport.kt create mode 100644 tests/src/test/kotlin/org/mockito/kotlin/internal/ValueClassSupportTest.kt 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 e1f036c..023735f 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt @@ -26,7 +26,9 @@ package org.mockito.kotlin import org.mockito.ArgumentCaptor +import org.mockito.kotlin.internal.toKotlinType import org.mockito.kotlin.internal.createInstance +import org.mockito.kotlin.internal.valueClassInnerClass import java.lang.reflect.Array import kotlin.reflect.KClass import kotlin.reflect.KType @@ -164,14 +166,11 @@ class KArgumentCaptor(private val kType: KType) { private val captor: ArgumentCaptor = if (clazz.isValue && !kType.isMarkedNullable) { - val boxImpl = - clazz.java.declaredMethods - .single { it.name == "box-impl" && it.parameterCount == 1 } - boxImpl.parameters[0].type // is the boxed type of the value type + clazz.valueClassInnerClass() } else { - clazz.java + clazz }.let { - ArgumentCaptor.forClass(it) + ArgumentCaptor.forClass(it.java) } /** @@ -179,38 +178,38 @@ class KArgumentCaptor(private val kType: KType) { * @throws IndexOutOfBoundsException if the value is not available. */ val firstValue: T - get() = toKotlinType(captor.firstValue) + get() = captor.firstValue.toKotlinType(clazz) /** * The second captured value of the argument. * @throws IndexOutOfBoundsException if the value is not available. */ val secondValue: T - get() = toKotlinType(captor.secondValue) + get() = captor.secondValue.toKotlinType(clazz) /** * The third captured value of the argument. * @throws IndexOutOfBoundsException if the value is not available. */ val thirdValue: T - get() = toKotlinType(captor.thirdValue) + get() = captor.thirdValue.toKotlinType(clazz) /** * The last captured value of the argument. * @throws IndexOutOfBoundsException if the value is not available. */ val lastValue: T - get() = toKotlinType(captor.lastValue) + get() = captor.lastValue.toKotlinType(clazz) /** * 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() = toKotlinType(captor.singleValue) + get() = captor.singleValue.toKotlinType(clazz) val allValues: List - get() = captor.allValues.map(::toKotlinType) + get() = captor.allValues.map { it.toKotlinType(clazz) } @Suppress("UNCHECKED_CAST") fun capture(): T { @@ -221,7 +220,7 @@ class KArgumentCaptor(private val kType: KType) { // 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 toKotlinType(captor.capture()) ?: if (clazz.java.isArray) { + return captor.capture().toKotlinType(clazz) ?: if (clazz.java.isArray) { singleElementArray() } else { createInstance(clazz) @@ -229,22 +228,6 @@ class KArgumentCaptor(private val kType: KType) { } private fun singleElementArray(): Any? = Array.newInstance(clazz.java.componentType, 1) - - @Suppress("UNCHECKED_CAST") - private fun toKotlinType(rawCapturedValue: Any?): T { - if (rawCapturedValue == null) return null as T - - if (clazz.isValue && rawCapturedValue::class != clazz) { - return rawCapturedValue - .let { - val boxImpl = - clazz.java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 } - boxImpl.invoke(null, it) - } as T - } - - return rawCapturedValue as T - } } val ArgumentCaptor.firstValue: T diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt index 39c69a4..89b645a 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt @@ -27,9 +27,12 @@ package org.mockito.kotlin import org.mockito.ArgumentMatcher import org.mockito.ArgumentMatchers +import org.mockito.kotlin.internal.boxAsValueClass import org.mockito.kotlin.internal.createInstance +import org.mockito.kotlin.internal.toJavaType +import org.mockito.kotlin.internal.toKotlinType +import org.mockito.kotlin.internal.valueClassInnerClass import kotlin.reflect.KClass -import kotlin.reflect.typeOf /** Object argument that is equal to the given value. */ inline fun eq(value: T): T { @@ -79,39 +82,15 @@ inline fun anyArray(): Array { } /** Matches any Kotlin value class with the same boxed type by taking its boxed type. */ -inline fun anyValueClass(): T { - require(T::class.isValue) { - "${T::class.qualifiedName} is not a value class." - } - - val boxImpl = - T::class.java.declaredMethods - .single { it.name == "box-impl" && it.parameterCount == 1 } - val boxedType = boxImpl.parameters[0].type - - return boxImpl.invoke(null, ArgumentMatchers.any(boxedType)) as T +inline fun anyValueClass(): T { + val clazz = T::class + return ArgumentMatchers.any(clazz.valueClassInnerClass().java).boxAsValueClass(clazz) } inline fun eqValueClass(value: T): T { - require(T::class.isValue) { - "${T::class.qualifiedName} is not a value class." - } - - if (typeOf().isMarkedNullable) { - // if the value is both value class and nullable, then Kotlin passes the value class boxed - // towards Mockito java code. - return ArgumentMatchers.eq(value) + return value.toJavaType().let { + (ArgumentMatchers.eq(it) ?: it).toKotlinType(T::class) } - - val unboxImpl = - T::class.java.declaredMethods - .single { it.name == "unbox-impl" && it.parameterCount == 0 } - val unboxed = unboxImpl.invoke(value) - - val boxImpl = - T::class.java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 } - - return boxImpl.invoke(null, ArgumentMatchers.eq(unboxed) ?: unboxed) as 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 b13186c..081fb7e 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 @@ -26,8 +26,6 @@ package org.mockito.kotlin.internal import kotlin.reflect.KClass -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.primaryConstructor inline fun createInstance(): T { return createInstance(T::class) @@ -48,13 +46,10 @@ fun createInstance(kClass: KClass): T { } } -@Suppress("UNCHECKED_CAST") private fun createInstanceNonPrimitive(kClass: KClass): T { return if (kClass.isValue) { - val boxImpl = - kClass.java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 } - val wrappedType = getValueClassWrappedType(kClass) - boxImpl.invoke(null, createInstance(wrappedType)) as T + createInstance(kClass.valueClassInnerClass()) + .boxAsValueClass(kClass) } else { castNull() } @@ -68,11 +63,3 @@ private fun createInstanceNonPrimitive(kClass: KClass): T { */ @Suppress("UNCHECKED_CAST") private fun castNull(): T = null as T - -private fun getValueClassWrappedType(kClass: KClass<*>): KClass<*> { - require(kClass.isValue) - - val primaryConstructor = checkNotNull(kClass.primaryConstructor) - val wrappedType = primaryConstructor.parameters.single().type - return wrappedType.classifier as KClass<*> -} diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/ValueClassSupport.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/ValueClassSupport.kt new file mode 100644 index 0000000..cd32c09 --- /dev/null +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/ValueClassSupport.kt @@ -0,0 +1,81 @@ +/* + * The MIT License + * + * Copyright (c) 2018 Niek Haarman + * Copyright (c) 2007 Mockito contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.mockito.kotlin.internal + +import java.lang.reflect.Method +import kotlin.reflect.KClass +import kotlin.reflect.typeOf + +@Suppress("UNCHECKED_CAST") +fun Any?.toKotlinType(clazz: KClass<*>): T { + if (this == null) return null as T + + return if (clazz.isValue && this::class != clazz) { + this.boxAsValueClass(clazz) as T + } else { + this as T + } +} + +inline fun T.toJavaType(): Any? { + if (this == null) return null + + return if (this::class.isValue && !typeOf().isMarkedNullable) { + this.unboxValueClass() + } else { + // if the value is both value class and nullable, then Kotlin passes the value class boxed + // towards Mockito java code. + this + } +} + +@Suppress("UNCHECKED_CAST") +fun Any?.boxAsValueClass(clazz: KClass<*>): T { + require(clazz.isValue) { "${clazz.qualifiedName} is not a value class." } + + val boxImpl = clazz.boxImpl() + return boxImpl.invoke(null, this) as T +} + +fun Any.unboxValueClass(): Any { + val clazz = this::class + require(clazz.isValue) { "${clazz.qualifiedName} is not a value class." } + + val unboxImpl = + clazz.java.declaredMethods + .single { it.name == "unbox-impl" && it.parameterCount == 0 } + + return unboxImpl.invoke(this) +} + +fun KClass<*>.valueClassInnerClass(): KClass<*> { + require(isValue) { "$qualifiedName is not a value class." } + + return boxImpl().parameters[0].type.kotlin +} + +private fun KClass<*>.boxImpl(): Method = + java.declaredMethods.single { it.name == "box-impl" && it.parameterCount == 1 } diff --git a/tests/src/test/kotlin/org/mockito/kotlin/internal/ValueClassSupportTest.kt b/tests/src/test/kotlin/org/mockito/kotlin/internal/ValueClassSupportTest.kt new file mode 100644 index 0000000..3080d88 --- /dev/null +++ b/tests/src/test/kotlin/org/mockito/kotlin/internal/ValueClassSupportTest.kt @@ -0,0 +1,202 @@ +package org.mockito.kotlin.internal + +import com.nhaarman.expect.expect +import org.junit.Test +import test.PrimitiveValueClass +import test.ValueClass +import test.assertThrows +import kotlin.reflect.KClass + +class ValueClassSupportTest { + @Test + fun `toKotlinType should pass through null value`() { + /* Given */ + val value: String? = null + + /* When */ + val result: ValueClass? = value.toKotlinType(ValueClass::class) + + /* Then */ + expect(result).toBeNull() + } + + @Test + fun `toKotlinType should pass through non-value-class types`() { + /* Given */ + val value = "test" + + /* When */ + val result: String = value.toKotlinType(String::class) + + /* Then */ + expect(result).toBe(value) + } + + @Test + fun `toKotlinType should box value as value class`() { + /* Given */ + val value = "test" + + /* When */ + val result: ValueClass? = value.toKotlinType(ValueClass::class) + + /* Then */ + expect(result).toNotBeNull() + expect(result!!.content).toBe(value) + } + + @Test + fun `toKotlinType should not re-box value class value`() { + /* Given */ + val value = ValueClass("test") + + /* When */ + val result: ValueClass? = value.toKotlinType(ValueClass::class) + + /* Then */ + expect(result).toNotBeNull() + expect(result).toBe(value) + } + + @Test + fun `toJavaType should pass through null value`() { + /* Given */ + val value: String? = null + + /* When */ + val result: Any? = value.toJavaType() + + /* Then */ + expect(result).toBeNull() + } + + @Test + fun `toJavaType should unbox a value class type`() { + /* Given */ + val value = ValueClass("test") + + /* When */ + val result: Any? = value.toJavaType() + + /* Then */ + expect(result).toNotBeNull() + expect(result).toBeInstanceOf() + expect(result).toBe(value.content) + } + + @Test + fun `toJavaType should unbox a primitive value class type`() { + /* Given */ + val value = PrimitiveValueClass(123) + + /* When */ + val result: Any? = value.toJavaType() + + /* Then */ + expect(result).toNotBeNull() + expect(result).toBeInstanceOf() + expect(result).toBe(value.value) + } + + @Test + fun `toJavaType should not unbox a nullable value class type`() { + /* Given */ + val value = ValueClass("test") as ValueClass? + + /* When */ + val result: Any? = value.toJavaType() + + /* Then */ + expect(result).toNotBeNull() + expect(result).toBeInstanceOf() + expect((result as ValueClass).content).toBe(value!!.content) + } + + @Test + fun `boxAsValueClass should box non-value-class types`() { + /* Given */ + val value = "test" + + /* When */ + val result: ValueClass = value.boxAsValueClass(ValueClass::class) + + /* Then */ + expect(result).toNotBeNull() + expect((result).content).toBe(value) + } + + @Test + fun `boxAsValueClass should pass through null value`() { + /* Given */ + val value: String? = null + + /* When */ + val result: ValueClass? = value.boxAsValueClass(ValueClass::class) + + /* Then */ + expect(result).toBeNull() + } + + @Test + fun `boxAsValueClass should throw if target type is non-value-class`() { + /* Given */ + val value: String? = null + + /* When, Then */ + val exception = assertThrows { + value.boxAsValueClass(Int::class) + } + expect(exception.message).toBe("kotlin.Int is not a value class.") + } + + @Test + fun `unboxValueClass should unbox a value class type`() { + /* Given */ + val value = ValueClass("test") + + /* When */ + val result = value.unboxValueClass() + + /* Then */ + expect(result).toNotBeNull() + expect(result).toBeInstanceOf() + expect(result).toBe(value.content) + } + + @Test + fun `unboxValueClass should throw if source type is non-value-class`() { + /* Given */ + val value = "test" + + /* When, Then */ + val exception = assertThrows { + value.unboxValueClass() + } + expect(exception.message).toBe("kotlin.String is not a value class.") + } + + @Test + fun `valueClassInnerClass should yield the inner type of a value class type`() { + /* Given */ + val clazz = ValueClass::class + + /* When */ + val result = clazz.valueClassInnerClass() + + /* Then */ + expect(result).toNotBeNull() + expect(result).toBeInstanceOf>() + } + + @Test + fun `valueClassInnerClass should throw if source type is non-value-class`() { + /* Given */ + val clazz = String::class + + /* When, Then */ + val exception = assertThrows { + clazz.valueClassInnerClass() + } + expect(exception.message).toBe("kotlin.String is not a value class.") + } +} From 4cf4e007d74aa4bd77adf2924f3ca1a596e69ffe Mon Sep 17 00:00:00 2001 From: Mark Koops Date: Fri, 12 Dec 2025 22:06:43 +0100 Subject: [PATCH 3/4] Make Matchers.eqValueClass() a bit more lenient for value class arguments: let the matcher match with either boxed or unboxed value as actual call argument. --- .../kotlin/org/mockito/kotlin/Matchers.kt | 21 ++++--- .../kotlin/internal/ValueClassSupport.kt | 13 ----- .../kotlin/internal/ValueClassSupportTest.kt | 55 ------------------- tests/src/test/kotlin/test/MatchersTest.kt | 22 +++++++- 4 files changed, 34 insertions(+), 77 deletions(-) diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt index 89b645a..80f4b02 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt @@ -25,18 +25,19 @@ package org.mockito.kotlin +import org.mockito.AdditionalMatchers import org.mockito.ArgumentMatcher import org.mockito.ArgumentMatchers import org.mockito.kotlin.internal.boxAsValueClass import org.mockito.kotlin.internal.createInstance -import org.mockito.kotlin.internal.toJavaType import org.mockito.kotlin.internal.toKotlinType +import org.mockito.kotlin.internal.unboxValueClass import org.mockito.kotlin.internal.valueClassInnerClass import kotlin.reflect.KClass /** Object argument that is equal to the given value. */ inline fun eq(value: T): T { - if(T::class.isValue) + if (T::class.isValue) return eqValueClass(value) return ArgumentMatchers.eq(value) ?: value @@ -49,7 +50,7 @@ fun same(value: T): T { /** Matches any object, excluding nulls. */ inline fun any(): T { - if(T::class.isValue) + if (T::class.isValue) return anyValueClass() return ArgumentMatchers.any(T::class.java) ?: createInstance() @@ -88,9 +89,15 @@ inline fun anyValueClass(): T { } inline fun eqValueClass(value: T): T { - return value.toJavaType().let { - (ArgumentMatchers.eq(it) ?: it).toKotlinType(T::class) - } + require(value::class.isValue) { "${value::class.qualifiedName} is not a value class." } + + val unboxed = value?.unboxValueClass() + val matcher = AdditionalMatchers.or( + ArgumentMatchers.eq(value), + ArgumentMatchers.eq(unboxed) + ) + + return (matcher ?: unboxed).toKotlinType(T::class) } /** @@ -101,7 +108,7 @@ inline fun eqValueClass(value: T): T { */ inline fun argThat(noinline predicate: T.() -> Boolean): T { return ArgumentMatchers.argThat { arg: T? -> arg?.predicate() ?: false } ?: createInstance( - T::class + T::class ) } diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/ValueClassSupport.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/ValueClassSupport.kt index cd32c09..bdd8a7b 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/ValueClassSupport.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/ValueClassSupport.kt @@ -27,7 +27,6 @@ package org.mockito.kotlin.internal import java.lang.reflect.Method import kotlin.reflect.KClass -import kotlin.reflect.typeOf @Suppress("UNCHECKED_CAST") fun Any?.toKotlinType(clazz: KClass<*>): T { @@ -40,18 +39,6 @@ fun Any?.toKotlinType(clazz: KClass<*>): T { } } -inline fun T.toJavaType(): Any? { - if (this == null) return null - - return if (this::class.isValue && !typeOf().isMarkedNullable) { - this.unboxValueClass() - } else { - // if the value is both value class and nullable, then Kotlin passes the value class boxed - // towards Mockito java code. - this - } -} - @Suppress("UNCHECKED_CAST") fun Any?.boxAsValueClass(clazz: KClass<*>): T { require(clazz.isValue) { "${clazz.qualifiedName} is not a value class." } diff --git a/tests/src/test/kotlin/org/mockito/kotlin/internal/ValueClassSupportTest.kt b/tests/src/test/kotlin/org/mockito/kotlin/internal/ValueClassSupportTest.kt index 3080d88..54f0321 100644 --- a/tests/src/test/kotlin/org/mockito/kotlin/internal/ValueClassSupportTest.kt +++ b/tests/src/test/kotlin/org/mockito/kotlin/internal/ValueClassSupportTest.kt @@ -2,7 +2,6 @@ package org.mockito.kotlin.internal import com.nhaarman.expect.expect import org.junit.Test -import test.PrimitiveValueClass import test.ValueClass import test.assertThrows import kotlin.reflect.KClass @@ -58,60 +57,6 @@ class ValueClassSupportTest { expect(result).toBe(value) } - @Test - fun `toJavaType should pass through null value`() { - /* Given */ - val value: String? = null - - /* When */ - val result: Any? = value.toJavaType() - - /* Then */ - expect(result).toBeNull() - } - - @Test - fun `toJavaType should unbox a value class type`() { - /* Given */ - val value = ValueClass("test") - - /* When */ - val result: Any? = value.toJavaType() - - /* Then */ - expect(result).toNotBeNull() - expect(result).toBeInstanceOf() - expect(result).toBe(value.content) - } - - @Test - fun `toJavaType should unbox a primitive value class type`() { - /* Given */ - val value = PrimitiveValueClass(123) - - /* When */ - val result: Any? = value.toJavaType() - - /* Then */ - expect(result).toNotBeNull() - expect(result).toBeInstanceOf() - expect(result).toBe(value.value) - } - - @Test - fun `toJavaType should not unbox a nullable value class type`() { - /* Given */ - val value = ValueClass("test") as ValueClass? - - /* When */ - val result: Any? = value.toJavaType() - - /* Then */ - expect(result).toNotBeNull() - expect(result).toBeInstanceOf() - expect((result as ValueClass).content).toBe(value!!.content) - } - @Test fun `boxAsValueClass should box non-value-class types`() { /* Given */ diff --git a/tests/src/test/kotlin/test/MatchersTest.kt b/tests/src/test/kotlin/test/MatchersTest.kt index 3b133cf..e8e3c62 100644 --- a/tests/src/test/kotlin/test/MatchersTest.kt +++ b/tests/src/test/kotlin/test/MatchersTest.kt @@ -619,7 +619,16 @@ class MatchersTest : TestBase() { } @Test - fun eqNullableValueClass() { + fun eqNullableValueClass_nullableArgument() { + val valueClass = ValueClass("Content") as ValueClass? + mock().apply { + nullableValueClass(valueClass) + verify(this).nullableValueClass(eq(valueClass)) + } + } + + @Test + fun eqNullableValueClass_nonNullableArgument() { val valueClass = ValueClass("Content") mock().apply { nullableValueClass(valueClass) @@ -637,7 +646,7 @@ class MatchersTest : TestBase() { } @Test - fun eqNullablePrimitiveValueClass() { + fun eqNullablePrimitiveValueClass_nullableArgument() { val primitiveValueClass = PrimitiveValueClass(123) as PrimitiveValueClass? mock().apply { nullablePrimitiveValueClass(primitiveValueClass) @@ -645,6 +654,15 @@ class MatchersTest : TestBase() { } } + @Test + fun eqNullablePrimitiveValueClass_nonNullableArgument() { + val primitiveValueClass = PrimitiveValueClass(123) + mock().apply { + nullablePrimitiveValueClass(primitiveValueClass) + verify(this).nullablePrimitiveValueClass(eq(primitiveValueClass)) + } + } + @Test fun eqNestedValueClass() { val nestedValueClass = NestedValueClass(ValueClass("Content")) From 40871000af2b7c9a39a0ef4f50f43f3d495a11a9 Mon Sep 17 00:00:00 2001 From: Mark Koops Date: Sat, 13 Dec 2025 19:55:54 +0100 Subject: [PATCH 4/4] Fix KArgumentCaptor for incompatibility with suspend function arguments. --- .../org/mockito/kotlin/ArgumentCaptor.kt | 101 ++++++++++++++---- .../test/kotlin/test/ArgumentCaptorTest.kt | 40 +++++++ tests/src/test/kotlin/test/Classes.kt | 3 + 3 files changed, 124 insertions(+), 20 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 023735f..0baedfc 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt @@ -36,13 +36,40 @@ import kotlin.reflect.typeOf /** * Creates a [KArgumentCaptor] for given type. + * + * Caution: this factory method cannot be used to create a captor for a suspend + * function, please refer to [suspendFunctionArgumentCaptor] for that. + * This incompatibility is caused by the use of `typeOf()` which is the way + * to determine runtime nullability of T, but this function is yet incompatible with + * suspend functions at compile time. That incompatibility has been declared since + * Kotlin 1.6, and the promised proper support for suspend functions has not been + * delivered ever since. + * See [Kotlin issue KT-47562](https://youtrack.jetbrains.com/issue/KT-47562/Support-suspend-functional-types-in-typeOf) + * for more details. */ inline fun argumentCaptor(): KArgumentCaptor { return KArgumentCaptor(typeOf()) } +/** + * Creates a [KArgumentCaptor] for given (suspend function). + */ +inline fun > suspendFunctionArgumentCaptor(): KArgumentCaptor { + return KArgumentCaptor(T::class) +} + /** * Creates 2 [KArgumentCaptor]s for given types. + * + * Caution: this factory method cannot be used to create a captor for a suspend + * function, please refer to [suspendFunctionArgumentCaptor] for that. + * This incompatibility is caused by the use of `typeOf()` which is the way + * to determine runtime nullability of T, but this function is yet incompatible with + * suspend functions at compile time. That incompatibility has been declared since + * Kotlin 1.6, and the promised proper support for suspend functions has not been + * delivered ever since. + * See [Kotlin issue KT-47562](https://youtrack.jetbrains.com/issue/KT-47562/Support-suspend-functional-types-in-typeOf) + * for more details. */ inline fun argumentCaptor( @Suppress("unused") a: KClass = A::class, @@ -56,6 +83,16 @@ inline fun argumentCaptor( /** * Creates 3 [KArgumentCaptor]s for given types. + * + * Caution: this factory method cannot be used to create a captor for a suspend + * function, please refer to [suspendFunctionArgumentCaptor] for that. + * This incompatibility is caused by the use of `typeOf()` which is the way + * to determine runtime nullability of T, but this function is yet incompatible with + * suspend functions at compile time. That incompatibility has been declared since + * Kotlin 1.6, and the promised proper support for suspend functions has not been + * delivered ever since. + * See [Kotlin issue KT-47562](https://youtrack.jetbrains.com/issue/KT-47562/Support-suspend-functional-types-in-typeOf) + * for more details. */ inline fun argumentCaptor( @Suppress("unused") a: KClass = A::class, @@ -75,30 +112,24 @@ class ArgumentCaptorHolder4( val third: C, val fourth: D ) { - - operator fun component1() = first - operator fun component2() = second - operator fun component3() = third - operator fun component4() = fourth -} - -class ArgumentCaptorHolder5( - val first: A, - val second: B, - val third: C, - val fourth: D, - val fifth: E -) { - operator fun component1() = first operator fun component2() = second operator fun component3() = third operator fun component4() = fourth - operator fun component5() = fifth } /** * Creates 4 [KArgumentCaptor]s for given types. + * + * Caution: this factory method cannot be used to create a captor for a suspend + * function, please refer to [suspendFunctionArgumentCaptor] for that. + * This incompatibility is caused by the use of `typeOf()` which is the way + * to determine runtime nullability of T, but this function is yet incompatible with + * suspend functions at compile time. That incompatibility has been declared since + * Kotlin 1.6, and the promised proper support for suspend functions has not been + * delivered ever since. + * See [Kotlin issue KT-47562](https://youtrack.jetbrains.com/issue/KT-47562/Support-suspend-functional-types-in-typeOf) + * for more details. */ inline fun argumentCaptor( @Suppress("unused") a: KClass = A::class, @@ -114,8 +145,32 @@ inline fun ) } +class ArgumentCaptorHolder5( + val first: A, + val second: B, + val third: C, + val fourth: D, + val fifth: E +) { + operator fun component1() = first + operator fun component2() = second + operator fun component3() = third + operator fun component4() = fourth + operator fun component5() = fifth +} + /** - * Creates 4 [KArgumentCaptor]s for given types. + * Creates 5 [KArgumentCaptor]s for given types. + * + * Caution: this factory method cannot be used to create a captor for a suspend + * function, please refer to [suspendFunctionArgumentCaptor] for that. + * This incompatibility is caused by the use of `typeOf()` which is the way + * to determine runtime nullability of T, but this function is yet incompatible with + * suspend functions at compile time. That incompatibility has been declared since + * Kotlin 1.6, and the promised proper support for suspend functions has not been + * delivered ever since. + * See [Kotlin issue KT-47562](https://youtrack.jetbrains.com/issue/KT-47562/Support-suspend-functional-types-in-typeOf) + * for more details. */ inline fun argumentCaptor( @Suppress("unused") a: KClass = A::class, @@ -161,11 +216,17 @@ inline fun capture(captor: ArgumentCaptor): T { return captor.capture() ?: createInstance() } -class KArgumentCaptor(private val kType: KType) { - private val clazz = kType.classifier as KClass<*> +class KArgumentCaptor( + private val clazz: KClass<*>, + private val isMarkedNullable: Boolean = false +) { + constructor(kType: KType):this( + kType.classifier as KClass<*>, + kType.isMarkedNullable + ) private val captor: ArgumentCaptor = - if (clazz.isValue && !kType.isMarkedNullable) { + if (clazz.isValue && !isMarkedNullable) { clazz.valueClassInnerClass() } else { clazz diff --git a/tests/src/test/kotlin/test/ArgumentCaptorTest.kt b/tests/src/test/kotlin/test/ArgumentCaptorTest.kt index d1025f2..be0d54e 100644 --- a/tests/src/test/kotlin/test/ArgumentCaptorTest.kt +++ b/tests/src/test/kotlin/test/ArgumentCaptorTest.kt @@ -2,6 +2,7 @@ package test import com.nhaarman.expect.expect import com.nhaarman.expect.expectErrorWithMessage +import kotlinx.coroutines.runBlocking import org.junit.Test import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.any @@ -9,6 +10,7 @@ import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doNothing import org.mockito.kotlin.mock import org.mockito.kotlin.nullableArgumentCaptor +import org.mockito.kotlin.suspendFunctionArgumentCaptor import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -420,4 +422,42 @@ class ArgumentCaptorTest : TestBase() { verify(m).nullablePrimitiveValueClass(captor.capture()) expect(captor.firstValue).toBe(valueClass) } + + @Test + fun argumentCaptor_function() { + /* Given */ + var counter = 0 + val m: SynchronousFunctions = mock() + val function: () -> Unit = { + counter++ + } + + /* When */ + m.functionArgument(function) + + /* Then */ + val captor = argumentCaptor<() -> Unit>() + verify(m).functionArgument(captor.capture()) + captor.firstValue.invoke() + expect(counter).toBe(1) + } + + @Test + fun argumentCaptor_suspend_function() { + /* Given */ + var counter = 0 + val m: SynchronousFunctions = mock() + val function: suspend () -> Unit = suspend { + counter++ + } + + /* When */ + m.suspendFunctionArgument(function) + + /* Then */ + val captor = suspendFunctionArgumentCaptor Unit>() + verify(m).suspendFunctionArgument(captor.capture()) + runBlocking { captor.firstValue.invoke() } + expect(counter).toBe(1) + } } diff --git a/tests/src/test/kotlin/test/Classes.kt b/tests/src/test/kotlin/test/Classes.kt index a7fdba8..bfc719e 100644 --- a/tests/src/test/kotlin/test/Classes.kt +++ b/tests/src/test/kotlin/test/Classes.kt @@ -105,6 +105,9 @@ interface SynchronousFunctions { fun nestedValueClassResult(): NestedValueClass fun primitiveValueClassResult(): PrimitiveValueClass fun nullablePrimitiveValueClassResult(): PrimitiveValueClass? + + fun functionArgument(function: () -> Unit) + fun suspendFunctionArgument(function: suspend () -> Unit) } interface SuspendFunctions {