Skip to content

Commit

Permalink
Merge pull request #1234 from T45K/introduce_verifyCount_DSL
Browse files Browse the repository at this point in the history
Introduce `verifyCount` DSL
  • Loading branch information
Raibaz committed Mar 20, 2024
2 parents 51f65d4 + b11b732 commit fe0e306
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 24 deletions.
20 changes: 20 additions & 0 deletions README.md
Expand Up @@ -674,6 +674,26 @@ verify(exactly = 0) { car.accelerate(fromSpeed = 30, toSpeed = 10) } // means no
confirmVerified(car)
```

Or you can use `verifyCount`:

```kotlin

val car = mockk<Car>(relaxed = true)

car.accelerate(fromSpeed = 10, toSpeed = 20)
car.accelerate(fromSpeed = 10, toSpeed = 30)
car.accelerate(fromSpeed = 20, toSpeed = 30)

// all pass
verifyCount {
(3..5) * { car.accelerate(allAny(), allAny()) } // same as verify(atLeast = 3, atMost = 5) { car.accelerate(allAny(), allAny()) }
1 * { car.accelerate(fromSpeed = 10, toSpeed = 20) } // same as verify(exactly = 1) { car.accelerate(fromSpeed = 10, toSpeed = 20) }
0 * { car.accelerate(fromSpeed = 30, toSpeed = 10) } // same as verify(exactly = 0) { car.accelerate(fromSpeed = 30, toSpeed = 10) }
}

confirmVerified(car)
```

### Verification order

* `verifyAll` verifies that all calls happened without checking their order.
Expand Down
12 changes: 12 additions & 0 deletions modules/mockk-dsl/api/mockk-dsl.api
Expand Up @@ -531,6 +531,18 @@ public final class io/mockk/MockKAssertScope {
public final fun getActual ()Ljava/lang/Object;
}

public final class io/mockk/MockKCallCountCoVerificationScope {
public fun <init> ()V
public final fun times (ILkotlin/jvm/functions/Function2;)V
public final fun times (Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function2;)V
}

public final class io/mockk/MockKCallCountVerificationScope {
public fun <init> ()V
public final fun times (ILkotlin/jvm/functions/Function1;)V
public final fun times (Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;)V
}

public final class io/mockk/MockKCancellationRegistry {
public static final field INSTANCE Lio/mockk/MockKCancellationRegistry;
public final fun cancelAll ()V
Expand Down
61 changes: 57 additions & 4 deletions modules/mockk-dsl/src/commonMain/kotlin/io/mockk/API.kt
Expand Up @@ -690,9 +690,10 @@ open class MockKMatcherScope(
val lambda: CapturingSlot<Function<*>>
) {

fun <T: Any> match(matcher: Matcher<T>, kclass: KClass<T>): T {
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 @@ -720,6 +721,7 @@ open class MockKMatcherScope(
*/
inline fun <reified T : Any> eq(value: T, inverse: Boolean = false): T =
match(EqMatcher(value, inverse = inverse))

/**
* Matches if the value is not equal to the provided [value] via the `deepEquals` function.
*/
Expand All @@ -730,6 +732,7 @@ open class MockKMatcherScope(
*/
inline fun <reified T : Any> refEq(value: T, inverse: Boolean = false): T =
match(EqMatcher(value, ref = true, inverse = inverse))

/**
* Matches if the value is not equal to the provided [value] via reference comparison.
*/
Expand All @@ -738,13 +741,15 @@ open class MockKMatcherScope(
/**
* Matches any argument given a [KClass]
*/
fun <T: Any> any(classifier: KClass<T>): T = match(ConstantMatcher(true), classifier)
fun <T : Any> any(classifier: KClass<T>): T = match(ConstantMatcher(true), classifier)

/**
* Matches any argument.
*/
inline fun <reified T : Any> any(): T = match(ConstantMatcher(true))

inline fun <reified T : Any> capture(lst: MutableList<T>): T = match(CaptureMatcher(lst, T::class))

/**
* Captures a non-nullable value to a [CapturingSlot].
*
Expand Down Expand Up @@ -782,7 +787,8 @@ open class MockKMatcherScope(
* network.download("testfile")
* // slot.captured is now "testfile"
*/
inline fun <reified T : Any> captureNullable(lst: CapturingSlot<T?>): T? = match(CapturingNullableSlotMatcher(lst, T::class))
inline fun <reified T : Any> captureNullable(lst: CapturingSlot<T?>): T? =
match(CapturingNullableSlotMatcher(lst, T::class))

/**
* Captures a nullable value to a [MutableList].
Expand All @@ -797,12 +803,14 @@ open class MockKMatcherScope(
* Matches if the value is equal to the provided [value] via the `compareTo` function.
*/
inline fun <reified T : Comparable<T>> cmpEq(value: T): T = match(ComparingMatcher(value, 0, T::class))

/**
* Matches if the value is more than the provided [value] via the `compareTo` function.
* @param andEquals matches more than or equal to
*/
inline fun <reified T : Comparable<T>> more(value: T, andEquals: Boolean = false): T =
match(ComparingMatcher(value, if (andEquals) 2 else 1, T::class))

/**
* Matches if the value is less than the provided [value] via the `compareTo` function.
* @param andEquals matches less than or equal to
Expand All @@ -824,10 +832,12 @@ open class MockKMatcherScope(
* Combines two matchers via a logical and.
*/
inline fun <reified T : Any> and(left: T, right: T): T = match(AndOrMatcher<T>(true, left, right))

/**
* Combines two matchers via a logical or.
*/
inline fun <reified T : Any> or(left: T, right: T): T = match(AndOrMatcher<T>(false, left, right))

/**
* Negates the matcher.
*/
Expand All @@ -840,6 +850,7 @@ open class MockKMatcherScope(
*/
inline fun <reified T : Any> isNull(inverse: Boolean = false): T = match(NullCheckMatcher<T>(inverse))
inline fun <reified T : Any, R : T> ofType(cls: KClass<R>): T = match(OfTypeMatcher<T>(cls))

/**
* Checks if the value belongs to the type.
*/
Expand Down Expand Up @@ -2182,6 +2193,48 @@ class MockKVerificationScope(
}
}

/**
* Part of DSL. Additional operations for call count verification scope.
*/
class MockKCallCountVerificationScope {
operator fun Int.times(verifyBlock: MockKVerificationScope.() -> Unit) {
MockKGateway.implementation().verifier.verify(
VerificationParameters(Ordering.UNORDERED, min = this, max = this, inverse = false, timeout = 0),
verifyBlock,
null
)
}

operator fun IntRange.times(verifyBlock: MockKVerificationScope.() -> Unit) {
MockKGateway.implementation().verifier.verify(
VerificationParameters(Ordering.UNORDERED, min = this.first, max = this.last, inverse = false, timeout = 0),
verifyBlock,
null
)
}
}

/**
* Part of DSL. Additional operations for coroutine call count verification scope.
*/
class MockKCallCountCoVerificationScope {
operator fun Int.times(verifyBlock: suspend MockKVerificationScope.() -> Unit) {
MockKGateway.implementation().verifier.verify(
VerificationParameters(Ordering.UNORDERED, min = this, max = this, inverse = false, timeout = 0),
null,
verifyBlock
)
}

operator fun IntRange.times(verifyBlock: suspend MockKVerificationScope.() -> Unit) {
MockKGateway.implementation().verifier.verify(
VerificationParameters(Ordering.UNORDERED, min = this.first, max = this.last, inverse = false, timeout = 0),
null,
verifyBlock
)
}
}

/**
* Part of DSL. Object to represent phrase "wasNot Called"
*/
Expand Down Expand Up @@ -2675,7 +2728,7 @@ class CapturingSlot<T : Any?> {
* For init state (not yet captured) returns false.
*/
val isNull
get() = when(val value = capturedValue) {
get() = when (val value = capturedValue) {
is CapturedValue.Value -> value.value == null
is CapturedValue.NotYetCaptured -> false
}
Expand Down
2 changes: 2 additions & 0 deletions modules/mockk/api/mockk.api
Expand Up @@ -38,6 +38,7 @@ public final class io/mockk/MockKKt {
public static synthetic fun coVerify$default (Lio/mockk/Ordering;ZIIIJLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static final fun coVerifyAll (ZLkotlin/jvm/functions/Function2;)V
public static synthetic fun coVerifyAll$default (ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static final fun coVerifyCount (Lkotlin/jvm/functions/Function1;)V
public static final fun coVerifyOrder (ZLkotlin/jvm/functions/Function2;)V
public static synthetic fun coVerifyOrder$default (ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static final fun coVerifySequence (ZLkotlin/jvm/functions/Function2;)V
Expand Down Expand Up @@ -72,6 +73,7 @@ public final class io/mockk/MockKKt {
public static synthetic fun verify$default (Lio/mockk/Ordering;ZIIIJLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun verifyAll (ZLkotlin/jvm/functions/Function1;)V
public static synthetic fun verifyAll$default (ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun verifyCount (Lkotlin/jvm/functions/Function1;)V
public static final fun verifyOrder (ZLkotlin/jvm/functions/Function1;)V
public static synthetic fun verifyOrder$default (ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun verifySequence (ZLkotlin/jvm/functions/Function1;)V
Expand Down
34 changes: 34 additions & 0 deletions modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt
Expand Up @@ -261,6 +261,7 @@ fun coVerify(
* @see verify
* @see verifyOrder
* @see verifySequence
* @see verifyCount
*
* @param inverse when true, the verification will check that the behaviour specified did **not** happen
*/
Expand All @@ -278,6 +279,7 @@ fun verifyAll(
* @see verify
* @see verifyAll
* @see verifySequence
* @see verifyCount
*
* @param inverse when true, the verification will check that the behaviour specified did **not** happen
*/
Expand All @@ -295,6 +297,7 @@ fun verifyOrder(
* @see verify
* @see verifyOrder
* @see verifyAll
* @see verifyCount
*
* @param inverse when true, the verification will check that the behaviour specified did **not** happen
*/
Expand All @@ -305,6 +308,20 @@ fun verifySequence(
MockKDsl.internalVerifySequence(inverse, verifyBlock)
}

/**
* Verifies that calls and their count
*
* @see coVerifyCount Coroutine version
* @see verify
* @see verifyOrder
* @see verifyAll
* @see verifySequence
*
*/
fun verifyCount(verifyBlock: MockKCallCountVerificationScope.() -> Unit) = MockK.useImpl {
MockKCallCountVerificationScope().verifyBlock()
}

/**
* Verifies that all calls inside [verifyBlock] happened. **Does not** verify any order. Coroutine version
*
Expand All @@ -314,6 +331,7 @@ fun verifySequence(
* @see coVerify
* @see coVerifyOrder
* @see coVerifySequence
* @see coVerifyCount
*
* @param inverse when true, the verification will check that the behaviour specified did **not** happen
*/
Expand All @@ -332,6 +350,7 @@ fun coVerifyAll(
* @see coVerify
* @see coVerifyAll
* @see coVerifySequence
* @see coVerifyCount
*
* @param inverse when true, the verification will check that the behaviour specified did **not** happen
*/
Expand All @@ -350,6 +369,7 @@ fun coVerifyOrder(
* @see coVerify
* @see coVerifyOrder
* @see coVerifyAll
* @see coVerifyCount
*
* @param inverse when true, the verification will check that the behaviour specified did **not** happen
*/
Expand All @@ -360,6 +380,20 @@ fun coVerifySequence(
MockKDsl.internalCoVerifySequence(inverse, verifyBlock)
}

/**
* Verifies that calls and their count. Coroutine version
*
* @see verifyCount
* @see coVerify
* @see coVerifyOrder
* @see coVerifyAll
* @see coVerifySequence
*
*/
fun coVerifyCount(verifyBlock: MockKCallCountCoVerificationScope.() -> Unit) = MockK.useImpl {
MockKCallCountCoVerificationScope().verifyBlock()
}

/**
* Exclude calls from recording
*
Expand Down
Expand Up @@ -6,6 +6,7 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import io.mockk.verifyCount
import io.mockk.verifyOrder
import io.mockk.verifySequence
import kotlin.test.Test
Expand Down Expand Up @@ -77,4 +78,22 @@ class CaptureSubclassVerificationTest {
assertTrue(slot.isCaptured)
assertIs<Subclass2>(slot.captured)
}

@Test
fun `test count`() {
val service = mockk<Service> {
every { method(any()) } just Runs
}

service.method(Subclass1())
service.method(Subclass2())

val slot = slot<Subclass2>()
verifyCount {
2 * { service.method(any()) }
1 * { service.method(capture(slot)) }
}
assertTrue(slot.isCaptured)
assertIs<Subclass2>(slot.captured)
}
}
26 changes: 23 additions & 3 deletions modules/mockk/src/commonTest/kotlin/io/mockk/it/CoVerifyTest.kt
@@ -1,9 +1,15 @@
package io.mockk.it

import io.mockk.*
import kotlinx.coroutines.coroutineScope
import io.mockk.InternalPlatformDsl
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyCount
import io.mockk.coVerifyOrder
import io.mockk.coVerifySequence
import io.mockk.mockk
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.coroutineScope

class CoVerifyTest {
class MockCls {
Expand All @@ -12,7 +18,7 @@ class CoVerifyTest {

val mock = mockk<MockCls>()

fun doCalls() {
private fun doCalls() {
coEvery { mock.op(5) } returns 1
coEvery { mock.op(6) } returns 2
coEvery { mock.op(7) } returns 3
Expand Down Expand Up @@ -156,4 +162,18 @@ class CoVerifyTest {
mock.op(7)
}
}

@Test
fun verifyCount() {
doCalls()

coVerifyCount {
0 * { mock.op(4) } // not called
1 * { mock.op(5) } // called
(0..Int.MAX_VALUE) * { mock.op(6) } // called
(1..1) * { mock.op(7) } // called
(0..0) * { mock.op(8) } // not called
(0..1) * { mock.op(9) } // not called
}
}
}
Expand Up @@ -135,7 +135,7 @@ class VerificationErrorsTest {
}

@Test
fun callsNotMatchinVerificationSequence() {
fun callsNotMatchingVerificationSequence() {
expectVerificationError("calls are not exactly matching verification sequence", "MockCls.otherOp") {
every { mock.otherOp(1, any()) } answers { 2 + firstArg<Int>() }

Expand Down

0 comments on commit fe0e306

Please sign in to comment.