diff --git a/mockito-kotlin/build.gradle b/mockito-kotlin/build.gradle index def2266..b02a33b 100644 --- a/mockito-kotlin/build.gradle +++ b/mockito-kotlin/build.gradle @@ -16,6 +16,7 @@ repositories { dependencies { compileOnly "org.jetbrains.kotlin:kotlin-stdlib" compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0' + implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.20" api "org.mockito:mockito-core:5.12.0" 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 14b3739..48bdb0c 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt @@ -42,6 +42,9 @@ fun same(value: T): T { /** Matches any object, excluding nulls. */ inline fun any(): T { + if(T::class.isValue) + return anyValueClass() + return ArgumentMatchers.any(T::class.java) ?: createInstance() } @@ -71,6 +74,21 @@ inline fun anyArray(): Array { return ArgumentMatchers.any(Array::class.java) ?: arrayOf() } +/** 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 boxImpls = T::class.java.declaredMethods.filter { it.name == "box-impl" && it.parameterCount == 1 } + require(boxImpls.size == 1) // Sanity check + + val boxImpl = boxImpls.first() + val boxedType = boxImpl.parameters[0].type + + return boxImpl.invoke(null, ArgumentMatchers.any(boxedType)) as T +} + /** * Creates a custom argument matcher. * `null` values will never evaluate to `true`. diff --git a/tests/src/test/kotlin/test/Classes.kt b/tests/src/test/kotlin/test/Classes.kt index 4d2f26e..6d78a05 100644 --- a/tests/src/test/kotlin/test/Classes.kt +++ b/tests/src/test/kotlin/test/Classes.kt @@ -81,8 +81,17 @@ interface Methods { fun argAndVararg(s: String, vararg a: String) fun nonDefaultReturnType(): ExtraInterface + + fun valueClass(v: ValueClass?) + fun nestedValueClass(v: NestedValueClass) } +@JvmInline +value class ValueClass(private val content: String) + +@JvmInline +value class NestedValueClass(val value: ValueClass) + interface ExtraInterface abstract class ThrowingConstructor { diff --git a/tests/src/test/kotlin/test/MatchersTest.kt b/tests/src/test/kotlin/test/MatchersTest.kt index 0fc47fa..10aeffc 100644 --- a/tests/src/test/kotlin/test/MatchersTest.kt +++ b/tests/src/test/kotlin/test/MatchersTest.kt @@ -319,6 +319,57 @@ class MatchersTest : TestBase() { } } + @Test + fun any_forValueClass() { + mock().apply { + valueClass(ValueClass("Content")) + verify(this).valueClass(any()) + } + } + + @Test + fun anyOrNull_forValueClass() { + mock().apply { + valueClass(ValueClass("Content")) + verify(this).valueClass(anyOrNull()) + } + } + + @Test + fun anyOrNull_forValueClass_withNull() { + mock().apply { + valueClass(null) + verify(this).valueClass(anyOrNull()) + } + } + + @Test + fun anyValueClass_withValueClass() { + mock().apply { + valueClass(ValueClass("Content")) + verify(this).valueClass(anyValueClass()) + } + } + + @Test + fun anyValueClass_withNonValueClass() { + expectErrorWithMessage("kotlin.Float is not a value class.") on { + mock().apply { + float(10f) + // Should throw an error because Float is not a value class + float(anyValueClass()) + } + } + } + + @Test + fun anyValueClass_withNestedValueClass() { + mock().apply { + nestedValueClass(NestedValueClass(ValueClass("Content"))) + verify(this).nestedValueClass(anyValueClass()) + } + } + /** * a VarargMatcher implementation for varargs of type [T] that will answer with type [R] if any of the var args * matched. Needs to keep state between matching invocations.