From e077021f44bb9d924cbe4d434aefdb17151d7541 Mon Sep 17 00:00:00 2001 From: Lars Andringa Date: Mon, 1 Jul 2024 13:20:58 +0200 Subject: [PATCH 1/4] Added value class support --- mockito-kotlin/build.gradle | 1 + .../kotlin/org/mockito/kotlin/Matchers.kt | 21 +++++++++++++++ tests/src/test/kotlin/test/Classes.kt | 5 ++++ tests/src/test/kotlin/test/MatchersTest.kt | 26 +++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/mockito-kotlin/build.gradle b/mockito-kotlin/build.gradle index 265c0d8b..096f0f42 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.7.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 14b37394..12164113 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.jvm.internal.impl.load.kotlin.JvmType.Object /** Object argument that is equal to the given value. */ fun eq(value: T): T { @@ -42,11 +43,17 @@ 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() } /** Matches anything, including nulls. */ inline fun anyOrNull(): T { + if(T::class.isValue) + return anyValueClass() + return ArgumentMatchers.any() ?: createInstance() } @@ -71,6 +78,20 @@ inline fun anyArray(): Array { return ArgumentMatchers.any(Array::class.java) ?: arrayOf() } +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 4d2f26e3..fdfad576 100644 --- a/tests/src/test/kotlin/test/Classes.kt +++ b/tests/src/test/kotlin/test/Classes.kt @@ -81,8 +81,13 @@ interface Methods { fun argAndVararg(s: String, vararg a: String) fun nonDefaultReturnType(): ExtraInterface + + fun valueClass(v: ValueClass) } +@JvmInline +value class ValueClass(private val content: String) + interface ExtraInterface abstract class ThrowingConstructor { diff --git a/tests/src/test/kotlin/test/MatchersTest.kt b/tests/src/test/kotlin/test/MatchersTest.kt index 0fc47faa..344605c3 100644 --- a/tests/src/test/kotlin/test/MatchersTest.kt +++ b/tests/src/test/kotlin/test/MatchersTest.kt @@ -319,6 +319,32 @@ class MatchersTest : TestBase() { } } + @Test + fun any_forValueClass() { + mock().apply { + valueClass(ValueClass("Content")) + verify(this).valueClass(any()) + } + } + + @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) + verify(this).float(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. From 42db378008a902939eb99cf272cbb3578dda56c1 Mon Sep 17 00:00:00 2001 From: Lars Andringa Date: Mon, 1 Jul 2024 13:28:18 +0200 Subject: [PATCH 2/4] Added doc --- mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt | 1 + 1 file changed, 1 insertion(+) 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 12164113..474f494d 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt @@ -78,6 +78,7 @@ 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." From b039fb10ba3966080c5b502d86267d5ba69e756d Mon Sep 17 00:00:00 2001 From: Lars Andringa Date: Thu, 4 Jul 2024 09:26:18 +0200 Subject: [PATCH 3/4] Remove unused import --- mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt | 1 - 1 file changed, 1 deletion(-) 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 474f494d..b2d46111 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt @@ -29,7 +29,6 @@ import org.mockito.ArgumentMatcher import org.mockito.ArgumentMatchers import org.mockito.kotlin.internal.createInstance import kotlin.reflect.KClass -import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType.Object /** Object argument that is equal to the given value. */ fun eq(value: T): T { From 348296b2a2cb7b15e8b41c4441e69953b0bc735d Mon Sep 17 00:00:00 2001 From: "l.s.andringa1" Date: Sat, 6 Jul 2024 00:50:54 +0200 Subject: [PATCH 4/4] Fixed tests, added more tests and simplified OrNull --- .../kotlin/org/mockito/kotlin/Matchers.kt | 3 --- tests/src/test/kotlin/test/Classes.kt | 6 ++++- tests/src/test/kotlin/test/MatchersTest.kt | 27 ++++++++++++++++++- 3 files changed, 31 insertions(+), 5 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 b2d46111..48bdb0ca 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt @@ -50,9 +50,6 @@ inline fun any(): T { /** Matches anything, including nulls. */ inline fun anyOrNull(): T { - if(T::class.isValue) - return anyValueClass() - return ArgumentMatchers.any() ?: createInstance() } diff --git a/tests/src/test/kotlin/test/Classes.kt b/tests/src/test/kotlin/test/Classes.kt index fdfad576..6d78a05b 100644 --- a/tests/src/test/kotlin/test/Classes.kt +++ b/tests/src/test/kotlin/test/Classes.kt @@ -82,12 +82,16 @@ interface Methods { fun nonDefaultReturnType(): ExtraInterface - fun valueClass(v: ValueClass) + 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 344605c3..10aeffca 100644 --- a/tests/src/test/kotlin/test/MatchersTest.kt +++ b/tests/src/test/kotlin/test/MatchersTest.kt @@ -327,6 +327,22 @@ class MatchersTest : TestBase() { } } + @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 { @@ -340,11 +356,20 @@ class MatchersTest : TestBase() { expectErrorWithMessage("kotlin.Float is not a value class.") on { mock().apply { float(10f) - verify(this).float(anyValueClass()) + // 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.