Skip to content

Add DSL keyword "repeatedly" which enables repeated answer - #1241

Open
ryu1-sakai wants to merge 1 commit into
mockk:masterfrom
ryu1-sakai:add-repeatedly
Open

ryu1-sakai wants to merge 1 commit into
mockk:masterfrom
ryu1-sakai:add-repeatedly

Conversation

@ryu1-sakai

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

Copy link
Copy Markdown

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

Raibaz commented Apr 30, 2024

Copy link
Copy Markdown
Collaborator

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

Raibaz commented Apr 30, 2024

Copy link
Copy Markdown
Collaborator

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

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

@ryu1-sakai

ryu1-sakai commented May 5, 2024

Copy link
Copy Markdown
Author

@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?

This branch has not been deployed

No deployments
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.

2 participants