Skip to content

Commit

Permalink
Merge pull request #1005 from ghackett/feature/reflection-any-matcher
Browse files Browse the repository at this point in the history
Add `fun <T> MockKMatcherScope.any(KClass<T>): T`
  • Loading branch information
Raibaz committed Jul 11, 2023
2 parents f559626 + 34743a7 commit ca59598
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,24 @@ inline fun <reified T : List<E>, E : Any> MockKMatcherScope.matchListWithoutOrde
): T = match(ListWithoutOrderMatcher(listOf(*items), refEq))
```

### Reflection matchers

Example using reflection to mock all methods on a builder-style object

```kotlin
val builderFunctions = MyBuilder::class.memberFunctions.filter { it.returnType.classifier == MyBuilder::class }
val builderMock = mockk<MyBuilder> {
builderFunctions.forEach { func ->
every {
val params = listOf<Any?>(builderMock) + func.parameters.drop(1).map { any(it.type.classifier as KClass<Any>) }
func.call(*params.toTypedArray())
} answers {
this@mockk
}
}
}
```

## Settings file

To adjust parameters globally, there are a few settings you can specify in a resource file.
Expand Down Expand Up @@ -1274,6 +1292,7 @@ By default, simple arguments are matched using `eq()`
| Matcher | Description |
|---------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
| `any()` | matches any argument |
| `any(Class)` | matches any argument of the give Class (for reflective mocking) |
| `allAny()` | special matcher that uses `any()` instead of `eq()` for matchers that are provided as simple arguments |
| `isNull()` | checks if the value is null |
| `isNull(inverse=true)` | checks if the value is not null |
Expand Down
2 changes: 2 additions & 0 deletions modules/mockk-dsl/api/mockk-dsl.api
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,7 @@ public abstract interface class io/mockk/MockKGateway$Verifier {

public class io/mockk/MockKMatcherScope {
public fun <init> (Lio/mockk/MockKGateway$CallRecorder;Lio/mockk/CapturingSlot;)V
public final fun any (Lkotlin/reflect/KClass;)Ljava/lang/Object;
public final fun anyBooleanVararg ()[Z
public final fun anyByteVararg ()[B
public final fun anyCharVararg ()[C
Expand All @@ -836,6 +837,7 @@ public class io/mockk/MockKMatcherScope {
public static synthetic fun hint$default (Lio/mockk/MockKMatcherScope;Ljava/lang/Object;Lkotlin/reflect/KClass;IILjava/lang/Object;)Ljava/lang/Object;
public final fun invoke (Ljava/lang/Object;Ljava/lang/String;)Lio/mockk/MockKMatcherScope$DynamicCallLong;
public final fun invokeNoArgs (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;
public final fun match (Lio/mockk/Matcher;Lkotlin/reflect/KClass;)Ljava/lang/Object;
public final fun setProperty (Ljava/lang/Object;Ljava/lang/String;)Lio/mockk/MockKMatcherScope$DynamicSetProperty;
public final fun varargAllBoolean (Lkotlin/jvm/functions/Function2;)[Z
public final fun varargAllByte (Lkotlin/jvm/functions/Function2;)[B
Expand Down
7 changes: 7 additions & 0 deletions modules/mockk-dsl/src/commonMain/kotlin/io/mockk/API.kt
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,9 @@ open class MockKMatcherScope(
val lambda: CapturingSlot<Function<*>>
) {

fun <T: Any> match(matcher: Matcher<T>, kclass: KClass<T>): T {
return callRecorder.matcher(matcher, kclass)
}
inline fun <reified T : Any> match(matcher: Matcher<T>): T {
return callRecorder.matcher(matcher, T::class)
}
Expand Down Expand Up @@ -732,6 +735,10 @@ open class MockKMatcherScope(
*/
inline fun <reified T : Any> nrefEq(value: T) = refEq(value, true)

/**
* Matches any argument given a [KClass]
*/
fun <T: Any> any(classifier: KClass<T>): T = match(ConstantMatcher(true), classifier)
/**
* Matches any argument.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
@file:Suppress("UNCHECKED_CAST")

package io.mockk

import kotlin.reflect.KClass
import kotlin.reflect.full.callSuspend
import kotlin.reflect.full.memberFunctions
import kotlin.test.Test


class ReflectiveMockTests {
private inline fun <reified T : Any> mockkBuilder(): T {
val builder: T = mockk()
builder.javaClass.kotlin.memberFunctions
.filter { it.returnType.classifier == T::class }
.filter { !it.isSuspend }
.forEach { func ->
every {
val params: List<Any?> =
listOf<Any?>(builder) + func.parameters.drop(1).map { any(it.type.classifier as KClass<Any>) }
func.call(*params.toTypedArray())
} answers { builder }
}
return builder
}

interface TestBuilder {
fun f1(p1: String): TestBuilder
fun f2(p1: String, p2: Int): TestBuilder
fun f3(p1: String, p2: Int, p3: Long?): TestBuilder
}

@Test
fun testAQuickMockBuilder() {
val builder: TestBuilder = mockkBuilder()

builder.f1("f1")
.f2("f2", 2)
.f3("f3", 3, 3)

verify {
builder.f1("f1")
builder.f2("f2", 2)
builder.f3("f3", 3, 3)
}
}


private inline fun <reified T : Any> mockkWithDefault(noinline defaultAnswer: MockKAnswerScope<Any?, Any?>.(Call) -> Any?): T {
val mock: T = mockk()
mock.javaClass.kotlin.memberFunctions
.filter { !it.isSuspend }
.forEach { func ->
every {
val params: List<Any?> =
listOf<Any?>(mock) + func.parameters.drop(1).map { any(it.type.classifier as KClass<Any>) }
func.call(*params.toTypedArray())
} answers {
this.defaultAnswer(it)
}
}

mock.javaClass.kotlin.memberFunctions
.filter { it.isSuspend }
.forEach { func ->
coEvery {
val params: List<Any?> =
listOf<Any?>(mock) + func.parameters.drop(1).map { any(it.type.classifier as KClass<Any>) }
func.callSuspend(*params.toTypedArray())
} answers {
this.defaultAnswer(it)
}
}
return mock
}

@Test fun testMockkWithDefault() {
val builder: TestBuilder = mockkWithDefault {
when (it.retType) {
TestBuilder::class -> it.invocation.self
Unit::class -> Unit
else -> null
}
}

builder.f1("f1")
.f2("f2", 2)
.f3("f3", 3, 3)

verify {
builder.f1("f1")
builder.f2("f2", 2)
builder.f3("f3", 3, 3)
}
}
}

0 comments on commit ca59598

Please sign in to comment.