lupuuss / Mokkery

The mocking library for Kotlin Multiplatform, easy to use, boilerplate-free and compiler plugin driven.
https://mokkery.dev
Apache License 2.0
136 stars 7 forks source link

Provide a way to block the return from a mocked suspend function #20

Open dalewking opened 3 weeks ago

dalewking commented 3 weeks ago

Sometimes you need to test a state that is present while a suspending call is being made. For example, loading is true while the suspending call is still being executed.

So you need a way to have a mocked suspend function not return immediately but only return when you tell it to. Here is the way i am handling it now, but it would be nice to have support for this built-in

I created a mutable map to record what calls are being blocked:

    val deferred = mutableMap<Any, CompletableDeferred<Unit>>()

    fun Any.whilePaused(block: () -> Unit) {
        try {
            deferred[this] = CompletableDeferred()
            block()
        }
        finally {
            deferred.remove(this)?.complete(Unit)
        }
    }

Then in any suspend mock set up that i need to support blocking I have to do this (In this case it is a fun interface being mocked):

    val fooBar =
        mock<FooBar> {
            everySuspend { invoke(any()) } calls {
                success(returnValue)
                    .also { deferred[this@mock]?.await() }
            }
        }

This then lets me do tests like this:

    fooBar.whilePaused {
        sut.doLoad()
        assertTrue(sut.loading)
    }

    assertFalse(sut.loading)
dalewking commented 3 weeks ago

Later realized I can use deferred[self]

I added these functions which handle the delaying for me, but would be nice to have it built in:

    infix fun <T> SuspendAnsweringScope<in Result<T>>.returnsSuccessBy(block: () -> T) {
        calls {
            try { success(block()) } finally { deferred[self]?.await() }
        }
    }

    infix fun <T> SuspendAnsweringScope<T>.callsDelayable(
        block: suspend SuspendCallDefinitionScope<T>.(CallArgs) -> T,
    ) {
        calls {
            try { block(this, it) } finally { deferred[self]?.await() }
        }
    }
lupuuss commented 3 weeks ago

I'm considering implementing a set of coroutine-related utilities, and such a util might be a part of that set. However, it would require changing kotlinx.coroutines to an api dependency. Before that I need to consider how it should be done.

Currently, Mokkery depends internally on coroutines - as an implementation dependency. I see 3 possible options:

Anyway, until I have this figured, any part of public API that uses coroutines API is kind of blocked.

CXwudi commented 3 days ago

I personally prefer 3rd option, moving all coroutines related stuff to a separate optional library called maybe like mockkery-coroutine-extension that user can optionally implement. MVIKotlin is one example library that does this. The core mvikotlin module doesn't have any coroutine related stuff, but the mvikotlin-extensions-coroutines does. Alternativelly, one can use mvikotlin-extensions-reaktive if coroutine is not the favourite

lupuuss commented 3 days ago

It's blocked by #7. Currently, wasm wasi support without coroutines causes some complications in the coroutines setup. With coroutines 1.9.0 release it will be simpler and then I can think about extracting coroutines module.

lupuuss commented 6 hours ago

@dalewking my API proposal for your use case:

val deferred = CompletableDeferred<Int>()
everySuspend { mock.bar() } awaits deferred

launch { sut.load() }

assertTrue(sut.isLoading)
deferred.complete(1)
assertFalse(sut.isLoading)

What do you think?