Skip to content

Commit 888e19d

Browse files
authored
Add value class support (#522)
Co-authored-by: Lars Andringa <[email protected]>
1 parent 6124e10 commit 888e19d

File tree

4 files changed

+79
-0
lines changed

4 files changed

+79
-0
lines changed

mockito-kotlin/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ repositories {
1616
dependencies {
1717
compileOnly "org.jetbrains.kotlin:kotlin-stdlib"
1818
compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
19+
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.20"
1920

2021
api "org.mockito:mockito-core:5.12.0"
2122

mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Matchers.kt

+18
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ fun <T> same(value: T): T {
4242

4343
/** Matches any object, excluding nulls. */
4444
inline fun <reified T : Any> any(): T {
45+
if(T::class.isValue)
46+
return anyValueClass()
47+
4548
return ArgumentMatchers.any(T::class.java) ?: createInstance()
4649
}
4750

@@ -71,6 +74,21 @@ inline fun <reified T : Any?> anyArray(): Array<T> {
7174
return ArgumentMatchers.any(Array<T>::class.java) ?: arrayOf()
7275
}
7376

77+
/** Matches any Kotlin value class with the same boxed type by taking its boxed type. */
78+
inline fun <reified T > anyValueClass(): T {
79+
require(T::class.isValue) {
80+
"${T::class.qualifiedName} is not a value class."
81+
}
82+
83+
val boxImpls = T::class.java.declaredMethods.filter { it.name == "box-impl" && it.parameterCount == 1 }
84+
require(boxImpls.size == 1) // Sanity check
85+
86+
val boxImpl = boxImpls.first()
87+
val boxedType = boxImpl.parameters[0].type
88+
89+
return boxImpl.invoke(null, ArgumentMatchers.any(boxedType)) as T
90+
}
91+
7492
/**
7593
* Creates a custom argument matcher.
7694
* `null` values will never evaluate to `true`.

tests/src/test/kotlin/test/Classes.kt

+9
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,17 @@ interface Methods {
8181
fun argAndVararg(s: String, vararg a: String)
8282

8383
fun nonDefaultReturnType(): ExtraInterface
84+
85+
fun valueClass(v: ValueClass?)
86+
fun nestedValueClass(v: NestedValueClass)
8487
}
8588

89+
@JvmInline
90+
value class ValueClass(private val content: String)
91+
92+
@JvmInline
93+
value class NestedValueClass(val value: ValueClass)
94+
8695
interface ExtraInterface
8796

8897
abstract class ThrowingConstructor {

tests/src/test/kotlin/test/MatchersTest.kt

+51
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,57 @@ class MatchersTest : TestBase() {
319319
}
320320
}
321321

322+
@Test
323+
fun any_forValueClass() {
324+
mock<Methods>().apply {
325+
valueClass(ValueClass("Content"))
326+
verify(this).valueClass(any())
327+
}
328+
}
329+
330+
@Test
331+
fun anyOrNull_forValueClass() {
332+
mock<Methods>().apply {
333+
valueClass(ValueClass("Content"))
334+
verify(this).valueClass(anyOrNull())
335+
}
336+
}
337+
338+
@Test
339+
fun anyOrNull_forValueClass_withNull() {
340+
mock<Methods>().apply {
341+
valueClass(null)
342+
verify(this).valueClass(anyOrNull())
343+
}
344+
}
345+
346+
@Test
347+
fun anyValueClass_withValueClass() {
348+
mock<Methods>().apply {
349+
valueClass(ValueClass("Content"))
350+
verify(this).valueClass(anyValueClass())
351+
}
352+
}
353+
354+
@Test
355+
fun anyValueClass_withNonValueClass() {
356+
expectErrorWithMessage("kotlin.Float is not a value class.") on {
357+
mock<Methods>().apply {
358+
float(10f)
359+
// Should throw an error because Float is not a value class
360+
float(anyValueClass())
361+
}
362+
}
363+
}
364+
365+
@Test
366+
fun anyValueClass_withNestedValueClass() {
367+
mock<Methods>().apply {
368+
nestedValueClass(NestedValueClass(ValueClass("Content")))
369+
verify(this).nestedValueClass(anyValueClass())
370+
}
371+
}
372+
322373
/**
323374
* a VarargMatcher implementation for varargs of type [T] that will answer with type [R] if any of the var args
324375
* matched. Needs to keep state between matching invocations.

0 commit comments

Comments
 (0)