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 DSL keyword "repeatedly" which enables repeated answer #1241

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions modules/mockk-dsl/api/mockk-dsl.api
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ public final class io/mockk/APIKt {
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/MockKRepeatedAnswerScope;Lio/mockk/Awaits;)Lio/mockk/MockKAdditionalAnswerScope;
public static final fun just (Lio/mockk/MockKRepeatedAnswerScope;Lio/mockk/Runs;)Lio/mockk/MockKAdditionalAnswerScope;
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;
Expand Down Expand Up @@ -922,6 +924,15 @@ public final class io/mockk/MockKObjectScope : io/mockk/MockKUnmockKScope {
public final fun getRecordPrivateCalls ()Z
}

public final class io/mockk/MockKRepeatedAnswerScope {
public fun <init> (Lio/mockk/MockKGateway$AnswerOpportunity;Lio/mockk/MockKGateway$CallRecorder;Lio/mockk/CapturingSlot;I)V
public final fun answers (Lio/mockk/Answer;)Lio/mockk/MockKAdditionalAnswerScope;
public final fun answers (Lkotlin/jvm/functions/Function2;)Lio/mockk/MockKAdditionalAnswerScope;
public final fun coAnswers (Lkotlin/jvm/functions/Function3;)Lio/mockk/MockKAdditionalAnswerScope;
public final fun returns (Ljava/lang/Object;)Lio/mockk/MockKAdditionalAnswerScope;
public final fun throws (Ljava/lang/Throwable;)Lio/mockk/MockKAdditionalAnswerScope;
}

public final class io/mockk/MockKSettings {
public static final field INSTANCE Lio/mockk/MockKSettings;
public final fun getRecordPrivateCalls ()Z
Expand Down Expand Up @@ -952,6 +963,7 @@ public final class io/mockk/MockKStubScope {
public final fun coAnswers (Lkotlin/jvm/functions/Function3;)Lio/mockk/MockKAdditionalAnswerScope;
public final fun nullablePropertyType (Lkotlin/reflect/KClass;)Lio/mockk/MockKStubScope;
public final fun propertyType (Lkotlin/reflect/KClass;)Lio/mockk/MockKStubScope;
public final fun repeatedly (I)Lio/mockk/MockKRepeatedAnswerScope;
public final fun returns (Ljava/lang/Object;)Lio/mockk/MockKAdditionalAnswerScope;
public final fun returnsArgument (I)Lio/mockk/MockKAdditionalAnswerScope;
public final fun returnsMany (Ljava/util/List;)Lio/mockk/MockKAdditionalAnswerScope;
Expand Down
42 changes: 42 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 @@ -2292,6 +2292,9 @@ class MockKStubScope<T, B>(
return MockKAdditionalAnswerScope(answerOpportunity, callRecorder, lambda)
}

infix fun repeatedly(times: Int): MockKRepeatedAnswerScope<T, B> =
MockKRepeatedAnswerScope(answerOpportunity, callRecorder, lambda, times)

infix fun returns(returnValue: T) = answers(ConstantAnswer(returnValue))

infix fun returnsMany(values: List<T>) = answers(ManyAnswersAnswer(values.allConst()))
Expand Down Expand Up @@ -2383,6 +2386,45 @@ infix fun MockKAdditionalAnswerScope<Unit, Unit>.andThenJust(runs: Runs) = andTh
@Suppress("UNUSED_PARAMETER")
infix fun <T, B> MockKAdditionalAnswerScope<T, B>.andThenJust(awaits: Awaits) = coAndThen { awaitCancellation() }

/**
* Scope to repeat an answer to reply. Part of DSL
*/
class MockKRepeatedAnswerScope<T, B>(
private val answerOpportunity: AnswerOpportunity<T>,
private val callRecorder: CallRecorder,
private val lambda: CapturingSlot<Function<*>>,
private val times: Int,
) {
infix fun answers(answer: Answer<T>): MockKAdditionalAnswerScope<T, B> {
repeat(times) {
answerOpportunity.provideAnswer(answer)
}
return MockKAdditionalAnswerScope(answerOpportunity, callRecorder, lambda)
}

infix fun answers(answer: MockKAnswerScope<T, B>.(Call) -> T): MockKAdditionalAnswerScope<T, B> =
answers(FunctionAnswer { MockKAnswerScope<T, B>(lambda, it).answer(it) })

infix fun returns(returnValue: T) = answers(ConstantAnswer(returnValue))

infix fun throws(ex: Throwable) = answers(ThrowingAnswer(ex))

infix fun coAnswers(answer: suspend MockKAnswerScope<T, B>.(Call) -> T) =
answers(CoFunctionAnswer { MockKAnswerScope<T, B>(lambda, it).answer(it) })
}

/**
* Part of DSL. Answer placeholder for Unit returning functions.
*/
@Suppress("UNUSED_PARAMETER")
infix fun MockKRepeatedAnswerScope<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> MockKRepeatedAnswerScope<T, B>.just(awaits: Awaits) = coAnswers { awaitCancellation() }

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

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.mockk.it

import io.mockk.*
import kotlinx.coroutines.coroutineScope
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class RepeatedlyAnswersTest {
private val mock = mockk<MockClass>()

@ParameterizedTest
@ValueSource(ints = [0, 1, 2, 3])
fun repeatedlyAnswers(times: Int) {
every { mock.op() } repeatedly times answers ConstantAnswer("repeating") andThen "final"

repeat(times) {
assertEquals("repeating", mock.op())
}
assertEquals("final", mock.op())
}

@ParameterizedTest
@ValueSource(ints = [0, 1, 2, 3])
fun repeatedlyAnswersLambda(times: Int) {
every { mock.op() } repeatedly times answers { "repeating" } andThen "final"

repeat(times) {
assertEquals("repeating", mock.op())
}
assertEquals("final", mock.op())
}

@ParameterizedTest
@ValueSource(ints = [0, 1, 2, 3])
fun repeatedlyReturns(times: Int) {
every { mock.op() } repeatedly times returns "repeating" andThen "final"

repeat(times) {
assertEquals("repeating", mock.op())
}
assertEquals("final", mock.op())
}

@ParameterizedTest
@ValueSource(ints = [0, 1, 2, 3])
fun repeatedlyThrows(times: Int) {
every { mock.op() } repeatedly times throws RuntimeException("repeating") andThen "final"

repeat(times) {
assertFailsWith(RuntimeException::class, message = "repeating") {
mock.op()
}
}
assertEquals("final", mock.op())
}

@ParameterizedTest
@ValueSource(ints = [0, 1, 2, 3])
fun repeatedlyCoAnswers(times: Int) {
every { mock.op() } repeatedly times coAnswers { coroutineScope { "repeating" } } andThen "final"

repeat(times) {
assertEquals("repeating", mock.op())
}
assertEquals("final", mock.op())
}

@ParameterizedTest
@ValueSource(ints = [0, 1, 2, 3])
fun repeatedlyJustRuns(times: Int) {
every { mock.unitOp() } repeatedly times just Runs andThenThrows RuntimeException("final")

repeat(times) {
assertDoesNotThrow {
mock.unitOp()
}
}
assertFailsWith(RuntimeException::class, message = "final") {
mock.unitOp()
}
}

private interface MockClass {
fun op(): String
fun unitOp()
}
}