Closed dalewking closed 3 months 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() }
}
}
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:
implementation
to api
and add coroutine utils to runtime. This option is easier, but I'm not sure if it's good.Anyway, until I have this figured, any part of public API that uses coroutines API is kind of blocked.
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
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.
@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?
My use case is slightly different. I know you have the idea that setting up mocks is done within the test classes. We have over 50 Use Case interfaces. Setting them up in individual tests is totally impractical.
So we have a MockUseCases class that sets up mock versions of all of them and creates a dependency injection module to bind all the mock implementations. A test class can subclass this and get mock implementations of all of the use cases
The class sets up var properties to define all the default implementations. If a test needs to change the behavior it can change the value of the property to be returned. They don't usually make calls to mokkery unless they need to throw an exception.
The problem with this proposal is that it doesn't work well with this predefined nature of the mocks. What you propose would require redefining the behavior of the mock within the test in order to add the deferred behavior.
It could work if you:
I've implemented the initial version of coroutine utils module. It contains a everySuspend { ... } awaits { deferred }
that fits your use case. It will be released in Mokkery 2.2.0
.
Sorry to report that the new functionality does not fit my use case at all unless I go through the step of redefining the mocking setup all over again like I mentioned in my last comment. Not too much of a problem for a simple use case that returns a simple value but for complex cases it is not practical and causes duplication
The fun <T> SuspendAnsweringScope<T>.awaits(provider: SuspendCallDefinitionScope<T>.(CallArgs) -> Deferred<T>)
is supposed to satisfy:
- allow a lambda to get the deferred at the time of the call
- the lambda would need a parameter of the mock
- support the deferred being optional
- this could be null on which you do nothing
- if not we can just return an already completed deferred
Could you explain why it's not sufficient?
You did not really satisfy the part about the deferred being optional.
As I have said I have a base class that sets up default mocking behavior for over 50 mock use cases. So in that base class i might have something like:
val someUseCase = mock<SomeUseCaseInterface> {
everySuspend { invoke(any()) } returnsSuccessBy {
// Here is 30 lines of code that computes the
// return value based on the input parameters and other variables
}
}
And say I have one place where all I need to do is prevent the mock from returning immediately so i can check a condition that should be true while the suspending call has started but not finished.
The way you have it the only way to really do that is to completely repeat the setup of the mock in that test. So in that test I would have to do something along the lines of:
val deferred = CompletableDeferred<Result<SomeType>>()
everySuspend { someUseCase(any()) } awaits deferred
// check condition
deferred.complete(
success(
// Here is 30 lines of code that computes the
// return value based on the input parameters and other variables
)
}
I already have defined the value I want returned. I do not want to re-define that all over again. I want to tell it to delay the return until I tell it to do the return
Optional Deferred
is problematic as then it's not clear what method should return. Also, one of your previous comment stated that if not we can just return an already completed deferred
and that is possible. However, by the last comment I don't think it's enough. If I understand you correctly, the potential answer API must have at least 2 parameters (probably lambdas with access to mocked method context). One responsible for optional suspension (that defaults to no suspension) and the other responsible for return value. I don't really have a good idea that satisfies this requirement, reduces the boilerplate and also it's not easily replaceable by calls
.
I think with either the callsCatching or the callback mechanism i described in #34 i can get to where I need to be. Where I am right now is:
infix fun <T> SuspendAnsweringScope<Result<T>>.callsCatching(
block: suspend (CallArgs) -> T,
) {
calls {
try {
success(block(it))
} catch (t: Throwable) {
failure(t)
} finally { await(self) }
}
}
where the await function optionally blocks based on other code. With calls catching I can simplify.
I wanted to stress that what you did add for blocking is valuable, just not for the way I was using blocking. It works fine for simple cases.
@dalewking PR welcome
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:
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):This then lets me do tests like this: