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

Conversation

ryu1-sakai
Copy link

@ryu1-sakai ryu1-sakai commented Apr 10, 2024

Summary

I'd like to add a new DSL keyword "repeatedly" which makes a mock to repeat a certain answer the specified number of times.

The usage is like the following.

every { mock.op() } repeatedly 3 answers { "repeating" } andThenAnswer { "final" }

The mock repeatedly replies "repeating" 3 times, and then replies "final".

Background

The reason why I'd like this feature is that I actually needed it when I made unit tests of a retry operation.

Let's say there are classes like the following.

class FailableOperation {
    fun runFailable(): Int
}

class RetryableOperation(
    private val failableOperation: FailableOperation,
    private val maxAttempts: Int,
) {
    fun runRetryable(): Int {
        // Call failableOperation.runFailable() with retry
    }
}

Then we want to make a test of RetryableOperation with the scenario where:

  1. failableOperation.runFailable() throws an exception several times less than maxAttempts.
  2. Then finally failableOperation.runFailable() returns an expected value.

We can make this test using existing features like the following, but the code not only doesn't look smart but also is not readable. (It takes an effort to know how many times the mock throws exception. 9 times? 10 times?)

@Test
fun runRetryable() {
    val failableOperation = mockK<FailableOperation>()
    val retryableOperation = RetryableOperation(failableOperation, maxAttempst = 10)

    var attempts = 0
    every { failableOperation.runFailable() } answers {
        attemps++
        if (attemps < 10) throws Exception()
        else 123
    }

    assertEquals(123, retryableOperation.runRetryable()

    verify(exactly = 10) { failableOperation.runFailable() }
}

Using the new keyword repeatedly, the test can look smart and be readable like the following. (Now it's easy to know that the mock throws an exception 9 times.)

@Test
fun runRetryable() {
    val failableOperation = mockK<FailableOperation>()
    val retryableOperation = RetryableOperation(failableOperation, 10)

    every { failableOperation.runFailable() } repeatedly 9 throws Exception() andThen 123

    assertEquals(123, retryableOperation.runRetryable()

    verify(exactly = 10) { failableOperation.runFailable() }
}

@Raibaz
Copy link
Collaborator

Raibaz commented Apr 30, 2024

Hey, thanks for looking into this and putting a PR together!

However, I'm a bit concerned that this would add some confusion to the existing DSL: isn't this implementing the behavior we already have for the returnsMany keyword (see here)?

@Raibaz
Copy link
Collaborator

Raibaz commented Apr 30, 2024

Specifically, what you are doing in your example can be written as

every { mock.op() } returnsMany List(3) { _ -> "repeating" }  andThenAnswer { "final" }

@ryu1-sakai
Copy link
Author

ryu1-sakai commented May 5, 2024

@Raibaz, thank you for your comment!
Yes, I often use returnsMany, and my example doesn't seem appropriate to explain the necessity of repeatedly.

As you pointed out, returnsMany is very useful when we want to make a mock return a static value multiple times or predefined values. However, it can't help when we wan to make a mock dynamically generate values multiple times.

Another idea to solve this issue is to add answersMany. Actually, we made and are using an extension function like the following in our company's project.

infix fun <T> MockKStubScope<T, T>.answersMany(answers: List<Answer<T>>): MockKAdditionalAnswerScope<T, *>

However, this solution has a downside. We need a function like the following to let compiler know the element type of the list given to answersMany.

fun <T> answerOf(answer: MockKAnswerScope<T, *>.(Call) -> T): Answer<T>

Then code to use returnsMany will be like the following. I feel like this code isn't cool because of existence of answerOf {}.

every { mock.op(any()) } answersMany List(3) { answerOf { generateValue(firstArg()) } } andThen ...

Do you have any idea? Do you feel answersMany looks better, or is there another better idea?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants