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..0baedfc 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/ArgumentCaptor.kt @@ -25,43 +25,84 @@ package org.mockito.kotlin -import org.mockito.kotlin.internal.createInstance 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 +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 argumentCaptor(): KArgumentCaptor { +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( - 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()) ) } /** * 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( - 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()) ) } @@ -71,13 +112,39 @@ class ArgumentCaptorHolder4( val third: C, val fourth: D ) { - operator fun component1() = first operator fun component2() = second operator fun component3() = third operator fun component4() = fourth } +/** + * 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, + @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(typeOf()), + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()), + KArgumentCaptor(typeOf()) + ) +} + class ArgumentCaptorHolder5( val first: A, val second: B, @@ -85,7 +152,6 @@ class ArgumentCaptorHolder5( val fourth: D, val fifth: E ) { - operator fun component1() = first operator fun component2() = second operator fun component3() = third @@ -94,38 +160,31 @@ 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 -): ArgumentCaptorHolder4, KArgumentCaptor, KArgumentCaptor, KArgumentCaptor> { - return ArgumentCaptorHolder4( - KArgumentCaptor(a), - KArgumentCaptor(b), - KArgumentCaptor(c), - KArgumentCaptor(d) - ) -} - -/** - * 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( - 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 +199,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,19 +216,22 @@ inline fun capture(captor: ArgumentCaptor): T { return captor.capture() ?: createInstance() } -class KArgumentCaptor ( - private val tClass: 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 (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 + if (clazz.isValue && !isMarkedNullable) { + clazz.valueClassInnerClass() } else { - tClass.java + clazz }.let { - ArgumentCaptor.forClass(it) + ArgumentCaptor.forClass(it.java) } /** @@ -177,38 +239,38 @@ class KArgumentCaptor ( * @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 { @@ -219,28 +281,14 @@ 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 captor.capture().toKotlinType(clazz) ?: if (clazz.java.isArray) { singleElementArray() } else { - createInstance(tClass) + createInstance(clazz) } as T } - 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 - } - } + private fun singleElementArray(): Any? = Array.newInstance(clazz.java.componentType, 1) } 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 e276c41..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,14 +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.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 @@ -45,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() @@ -78,33 +83,21 @@ 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." - } +inline fun eqValueClass(value: T): T { + require(value::class.isValue) { "${value::class.qualifiedName} is not a value 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 } + val unboxed = value?.unboxValueClass() + val matcher = AdditionalMatchers.or( + ArgumentMatchers.eq(value), + ArgumentMatchers.eq(unboxed) + ) - return boxImpl.invoke(null, ArgumentMatchers.eq(unboxed) ?: unboxed) as T + return (matcher ?: unboxed).toKotlinType(T::class) } /** @@ -115,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/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..bdd8a7b --- /dev/null +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/ValueClassSupport.kt @@ -0,0 +1,68 @@ +/* + * 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 + +@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 + } +} + +@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..54f0321 --- /dev/null +++ b/tests/src/test/kotlin/org/mockito/kotlin/internal/ValueClassSupportTest.kt @@ -0,0 +1,147 @@ +package org.mockito.kotlin.internal + +import com.nhaarman.expect.expect +import org.junit.Test +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 `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.") + } +} diff --git a/tests/src/test/kotlin/test/ArgumentCaptorTest.kt b/tests/src/test/kotlin/test/ArgumentCaptorTest.kt index d467088..be0d54e 100644 --- a/tests/src/test/kotlin/test/ArgumentCaptorTest.kt +++ b/tests/src/test/kotlin/test/ArgumentCaptorTest.kt @@ -2,9 +2,18 @@ package test import com.nhaarman.expect.expect import com.nhaarman.expect.expectErrorWithMessage -import org.junit.Ignore +import kotlinx.coroutines.runBlocking 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.suspendFunctionArgumentCaptor +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import java.util.* class ArgumentCaptorTest : TestBase() { @@ -385,7 +394,6 @@ class ArgumentCaptorTest : TestBase() { } @Test - @Ignore("See issue #555") fun argumentCaptor_primitive_value_class() { /* Given */ val m: SynchronousFunctions = mock() @@ -401,7 +409,6 @@ class ArgumentCaptorTest : TestBase() { } @Test - @Ignore("See issue #555") fun argumentCaptor_nullable_primitive_value_class() { /* Given */ val m: SynchronousFunctions = mock() @@ -411,8 +418,46 @@ class ArgumentCaptorTest : TestBase() { m.nullablePrimitiveValueClass(valueClass) /* Then */ - val captor = argumentCaptor() + val captor = argumentCaptor() 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 b93e2d2..bfc719e 100644 --- a/tests/src/test/kotlin/test/Classes.kt +++ b/tests/src/test/kotlin/test/Classes.kt @@ -98,11 +98,16 @@ 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? + + fun functionArgument(function: () -> Unit) + fun suspendFunctionArgument(function: suspend () -> Unit) } interface SuspendFunctions { @@ -118,6 +123,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..e8e3c62 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 @@ -620,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) @@ -629,12 +637,29 @@ class MatchersTest : TestBase() { } @Test - @Ignore("See issue #555") - fun eqNullablePrimitiveValueClass() { - val valueClass = PrimitiveValueClass(123) + fun eqPrimitiveValueClass() { + val primitiveValueClass = PrimitiveValueClass(123) + mock().apply { + primitiveValueClass(primitiveValueClass) + verify(this).primitiveValueClass(eq(primitiveValueClass)) + } + } + + @Test + fun eqNullablePrimitiveValueClass_nullableArgument() { + val primitiveValueClass = PrimitiveValueClass(123) as PrimitiveValueClass? + mock().apply { + nullablePrimitiveValueClass(primitiveValueClass) + verify(this).nullablePrimitiveValueClass(eq(primitiveValueClass)) + } + } + + @Test + fun eqNullablePrimitiveValueClass_nonNullableArgument() { + val primitiveValueClass = PrimitiveValueClass(123) 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 */