Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Awaits extensions similar to Runs to await suspend functions until cancelled #927

Merged
merged 6 commits into from Oct 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/gradle.yml
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: [ 11, 17, 19 ] # test LTS versions, and the newest
java-version: [ 11, 17, 18 ] # test LTS versions, and the newest
kotlin-version: [ 1.5.31, 1.6.21, 1.7.20-RC ]
kotlin-ir-enabled: [ true, false ]
fail-fast: false # in case one JDK fails, we still want to see results from others
Expand Down
97 changes: 58 additions & 39 deletions README.md
Expand Up @@ -696,7 +696,7 @@ confirmVerified(obj)

To double check that all calls were verified by `verify...` constructs, you can use `confirmVerified`:

```
```kotlin
confirmVerified(mock1, mock2)
```

Expand All @@ -706,7 +706,7 @@ It will throw an exception if there are some calls left without verification.

Some calls can be excluded from this confirmation, check the next section for more details.

```
```kotlin
val car = mockk<Car>()

every { car.drive(Direction.NORTH) } returns Outcome.OK
Expand All @@ -727,7 +727,7 @@ confirmVerified(car) // makes sure all calls were covered with verification

Because clean & maintainable test code requires zero unnecessary code, you can ensure that there is no unnecessary stubs.

```
```kotlin
checkUnnecessaryStub(mock1, mock2)
```

Expand All @@ -739,13 +739,13 @@ This can happen if you have declared some really unnecessary stubs or if the tes

To exclude unimportant calls from being recorded, you can use `excludeRecords`:

```
```kotlin
excludeRecords { mock.operation(any(), 5) }
```

All matching calls will be excluded from recording. This may be useful if you are using exhaustive verification: `verifyAll`, `verifySequence` or `confirmVerified`.

```
```kotlin
val car = mockk<Car>()

every { car.drive(Direction.NORTH) } returns Outcome.OK
Expand Down Expand Up @@ -854,6 +854,25 @@ car.drive(Direction.NORTH) // returns OK

coVerify { car.drive(Direction.NORTH) }
```

And to simulate a never returning `suspend` function, you can use `coJustAwait`:

```kotlin
runTest {
val car = mockk<Car>()

coJustAwait { car.drive(any()) } // car.drive(...) will never return

val job = launch(UnconfinedTestDispatcher()) {
car.drive(Direction.NORTH)
}

coVerify { car.drive(Direction.NORTH) }

job.cancelAndJoin() // Don't forget to cancel the job
}
```

### Extension functions

There are three types of extension function in Kotlin:
Expand Down Expand Up @@ -923,24 +942,24 @@ mockkStatic(Obj::squareValue)
If `@JvmName` is used, specify it as a class name.

KHttp.kt:
```
```kotlin
@file:JvmName("KHttp")

package khttp
// ... KHttp code
```

Testing code:
```
```kotlin
mockkStatic("khttp.KHttp")
```

Sometimes you need to know a little bit more to mock an extension function.
For example the extension function `File.endsWith()` has a totally unpredictable `classname`:
```kotlin
mockkStatic("kotlin.io.FilesKt__UtilsKt")
every { File("abc").endsWith(any<String>()) } returns true
println(File("abc").endsWith("abc"))
mockkStatic("kotlin.io.FilesKt__UtilsKt")
every { File("abc").endsWith(any<String>()) } returns true
println(File("abc").endsWith("abc"))
```
This is standard Kotlin behaviour that may be unpredictable.
Use `Tools -> Kotlin -> Show Kotlin Bytecode` or check `.class` files in JAR archive to detect such names.
Expand All @@ -950,37 +969,37 @@ Use `Tools -> Kotlin -> Show Kotlin Bytecode` or check `.class` files in JAR arc
From version 1.9.1, more extended vararg handling is possible:

```kotlin
interface ClsWithManyMany {
fun manyMany(vararg x: Any): Int
}
interface ClsWithManyMany {
fun manyMany(vararg x: Any): Int
}

val obj = mockk<ClsWithManyMany>()
val obj = mockk<ClsWithManyMany>()

every { obj.manyMany(5, 6, *varargAll { it == 7 }) } returns 3
every { obj.manyMany(5, 6, *varargAll { it == 7 }) } returns 3

println(obj.manyMany(5, 6, 7)) // 3
println(obj.manyMany(5, 6, 7, 7)) // 3
println(obj.manyMany(5, 6, 7, 7, 7)) // 3
println(obj.manyMany(5, 6, 7)) // 3
println(obj.manyMany(5, 6, 7, 7)) // 3
println(obj.manyMany(5, 6, 7, 7, 7)) // 3

every { obj.manyMany(5, 6, *anyVararg(), 7) } returns 4
every { obj.manyMany(5, 6, *anyVararg(), 7) } returns 4

println(obj.manyMany(5, 6, 1, 7)) // 4
println(obj.manyMany(5, 6, 2, 3, 7)) // 4
println(obj.manyMany(5, 6, 4, 5, 6, 7)) // 4
println(obj.manyMany(5, 6, 1, 7)) // 4
println(obj.manyMany(5, 6, 2, 3, 7)) // 4
println(obj.manyMany(5, 6, 4, 5, 6, 7)) // 4

every { obj.manyMany(5, 6, *varargAny { nArgs > 5 }, 7) } returns 5
every { obj.manyMany(5, 6, *varargAny { nArgs > 5 }, 7) } returns 5

println(obj.manyMany(5, 6, 4, 5, 6, 7)) // 5
println(obj.manyMany(5, 6, 4, 5, 6, 7, 7)) // 5
println(obj.manyMany(5, 6, 4, 5, 6, 7)) // 5
println(obj.manyMany(5, 6, 4, 5, 6, 7, 7)) // 5

every {
obj.manyMany(5, 6, *varargAny {
if (position < 3) it == 3 else it == 4
}, 7)
} returns 6
println(obj.manyMany(5, 6, 3, 4, 7)) // 6
println(obj.manyMany(5, 6, 3, 4, 4, 7)) // 6
every {
obj.manyMany(5, 6, *varargAny {
if (position < 3) it == 3 else it == 4
}, 7)
} returns 6

println(obj.manyMany(5, 6, 3, 4, 7)) // 6
println(obj.manyMany(5, 6, 3, 4, 4, 7)) // 6
```

### Private functions mocking / dynamic calls
Expand Down Expand Up @@ -1092,10 +1111,10 @@ every { quit(1) } throws Exception("this is a test")
A very simple way to create new matchers is by attaching a function
to `MockKMatcherScope` or `MockKVerificationScope` and using the `match` function:

```
fun MockKMatcherScope.seqEq(seq: Sequence<String>) = match<Sequence<String>> {
it.toList() == seq.toList()
}
```kotlin
fun MockKMatcherScope.seqEq(seq: Sequence<String>) = match<Sequence<String>> {
it.toList() == seq.toList()
}
```

It's also possible to create more advanced matchers by implementing the `Matcher` interface.
Expand All @@ -1105,7 +1124,6 @@ It's also possible to create more advanced matchers by implementing the `Matcher
Example of a custom matcher that compares list without order:

```kotlin

@Test
fun test() {
class MockCls {
Expand Down Expand Up @@ -1164,7 +1182,6 @@ inline fun <reified T : List<E>, E : Any> MockKMatcherScope.matchListWithoutOrde
vararg items: E,
refEq: Boolean = true
): T = match(ListWithoutOrderMatcher(listOf(*items), refEq))

```

## Settings file
Expand Down Expand Up @@ -1309,6 +1326,7 @@ An Answer can be followed up by one or more additional answers.
|`answers answerObj`|specify that the matched call answers with an Answer object|
|`answers { nothing }`|specify that the matched call answers null|
|`just Runs`|specify that the matched call is returning Unit (returns null)|
|`just Awaits`|specify that the matched call never returns (available since v1.13.3)|
|`propertyType Class`|specify the type of the backing field accessor|
|`nullablePropertyType Class`|specify the type of the backing field accessor as a nullable type|

Expand All @@ -1328,6 +1346,7 @@ So this is similar to the `returnsMany` semantics.
|`andThenAnswer answerObj`|specify that the matched call answers with an Answer object|
|`andThen { nothing }`|specify that the matched call answers null|
|`andThenJust Runs`|specify that the matched call is returning Unit (available since v1.12.2)|
|`andThenJust Awaits`|specify that the matched call is never returning (available since v1.13.3)|

### Answer scope

Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/buildsrc/config/Deps.kt
Expand Up @@ -46,5 +46,6 @@ object Deps {

const val kotlinCoroutinesBom = "org.jetbrains.kotlinx:kotlinx-coroutines-bom:${Versions.coroutines}"
const val kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core"
const val kotlinCoroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test"
}
}
6 changes: 6 additions & 0 deletions modules/mockk-dsl/api/mockk-dsl.api
@@ -1,9 +1,11 @@
public final class io/mockk/APIKt {
public static final fun andThenJust (Lio/mockk/MockKAdditionalAnswerScope;Lio/mockk/Awaits;)Lio/mockk/MockKAdditionalAnswerScope;
public static final fun andThenJust (Lio/mockk/MockKAdditionalAnswerScope;Lio/mockk/Runs;)Lio/mockk/MockKAdditionalAnswerScope;
public static final fun checkEquals (Lio/mockk/MockKAssertScope;Ljava/lang/Object;)V
public static final fun checkEquals (Lio/mockk/MockKAssertScope;Ljava/lang/String;Ljava/lang/Object;)V
public static final fun internalSubstitute (Ljava/lang/Object;Ljava/util/Map;)Ljava/lang/Object;
public static final fun internalSubstitute (Ljava/util/List;Ljava/util/Map;)Ljava/util/List;
public static final fun just (Lio/mockk/MockKStubScope;Lio/mockk/Awaits;)Lio/mockk/MockKAdditionalAnswerScope;
public static final fun just (Lio/mockk/MockKStubScope;Lio/mockk/Runs;)Lio/mockk/MockKAdditionalAnswerScope;
public static final fun use (Lio/mockk/Deregisterable;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static final fun use (Lio/mockk/MockKUnmockKScope;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
Expand Down Expand Up @@ -59,6 +61,10 @@ public final class io/mockk/ArrayMatcher : io/mockk/CapturingMatcher, io/mockk/M
public fun toString ()Ljava/lang/String;
}

public final class io/mockk/Awaits {
public static final field INSTANCE Lio/mockk/Awaits;
}

public final class io/mockk/BackingFieldValue {
public fun <init> (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V
public final fun getGetter ()Lkotlin/jvm/functions/Function0;
Expand Down
18 changes: 18 additions & 0 deletions modules/mockk-dsl/src/commonMain/kotlin/io/mockk/API.kt
Expand Up @@ -5,6 +5,7 @@ package io.mockk
import io.mockk.InternalPlatformDsl.toStr
import io.mockk.MockKGateway.*
import io.mockk.core.ValueClassSupport.boxedClass
import kotlinx.coroutines.awaitCancellation
import kotlin.coroutines.Continuation
import kotlin.reflect.KClass

Expand Down Expand Up @@ -2096,6 +2097,12 @@ private fun formatAssertMessage(actual: Any?, expected: Any?, message: String? =
object Runs
typealias runs = Runs

/**
* Part of DSL. Object to represent phrase "just Awaits"
*/
object Awaits
typealias awaits = Awaits

/**
* Stub scope. Part of DSL
*
Expand Down Expand Up @@ -2146,6 +2153,12 @@ class MockKStubScope<T, B>(
@Suppress("UNUSED_PARAMETER")
infix fun MockKStubScope<Unit, Unit>.just(runs: Runs) = answers(ConstantAnswer(Unit))

/**
* Part of DSL. Answer placeholder for never returning suspend functions.
*/
@Suppress("UNUSED_PARAMETER")
infix fun <T, B> MockKStubScope<T, B>.just(awaits: Awaits) = coAnswers { awaitCancellation() }

/**
* Scope to chain additional answers to reply. Part of DSL
*/
Expand Down Expand Up @@ -2184,6 +2197,11 @@ class MockKAdditionalAnswerScope<T, B>(
@Suppress("UNUSED_PARAMETER")
infix fun MockKAdditionalAnswerScope<Unit, Unit>.andThenJust(runs: Runs) = andThenAnswer(ConstantAnswer(Unit))

/**
* Part of DSL. Answer placeholder for never returning functions.
*/
@Suppress("UNUSED_PARAMETER")
infix fun <T, B> MockKAdditionalAnswerScope<T, B>.andThenJust(awaits: Awaits) = coAndThen { awaitCancellation() }

internal fun <T> List<T>.allConst() = this.map { ConstantAnswer(it) }

Expand Down
1 change: 1 addition & 0 deletions modules/mockk/api/mockk.api
Expand Up @@ -32,6 +32,7 @@ public final class io/mockk/MockKKt {
public static final fun coEvery (Lkotlin/jvm/functions/Function2;)Lio/mockk/MockKStubScope;
public static final fun coExcludeRecords (ZLkotlin/jvm/functions/Function2;)V
public static synthetic fun coExcludeRecords$default (ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static final fun coJustAwait (Lkotlin/jvm/functions/Function2;)Lio/mockk/MockKAdditionalAnswerScope;
public static final fun coJustRun (Lkotlin/jvm/functions/Function2;)Lio/mockk/MockKAdditionalAnswerScope;
public static final fun coVerify (Lio/mockk/Ordering;ZIIIJLkotlin/jvm/functions/Function2;)V
public static synthetic fun coVerify$default (Lio/mockk/Ordering;ZIIIJLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
Expand Down
1 change: 1 addition & 0 deletions modules/mockk/build.gradle.kts
Expand Up @@ -33,6 +33,7 @@ kotlin {
val commonTest by getting {
dependencies {
implementation(kotlin("test-junit5"))
implementation(Deps.Libs.kotlinCoroutinesTest)
}
}
val jvmMain by getting {
Expand Down
8 changes: 8 additions & 0 deletions modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt
Expand Up @@ -124,6 +124,14 @@ fun <T> coEvery(stubBlock: suspend MockKMatcherScope.() -> T): MockKStubScope<T,
*/
fun coJustRun(stubBlock: suspend MockKMatcherScope.() -> Unit) = coEvery(stubBlock) just Runs

/**
* Stub block to never return. Part of DSL.
*
* Used to define what behaviour is going to be mocked.
* @see [coJustRun]
*/
fun coJustAwait(stubBlock: suspend MockKMatcherScope.() -> Unit) = coEvery(stubBlock) just Awaits

/**
* Verifies that calls were made in the past. Part of DSL
*
Expand Down