Skip to content

Commit

Permalink
Merge pull request #927 from SimonMarquis/coJustAwait
Browse files Browse the repository at this point in the history
Add `Awaits` extensions similar to `Runs` to await suspend functions until cancelled
  • Loading branch information
Raibaz committed Oct 5, 2022
2 parents 6d5fe10 + 6803a08 commit ddf677e
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 40 deletions.
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

0 comments on commit ddf677e

Please sign in to comment.