Oleksii Malovanyi
06/10/2021, 3:51 PMdelay()
if occurs inside the intent
block? I’ve tried to pass TestCoroutineDispatcher
as the orbitDispatcher
and then advanceTimeBy
but it has no effect and I have to wait for the delay to finish 😞blocking
param from the test()
extensions and swap it with the dispatcher (unconfined) by default -> then it would be easy to use own TestCoroutineDispatcher
Mikolaj Leszczynski
06/11/2021, 7:25 AMblocking
as it’s not as simple as just setting a dispatcher - it also uses runBlocking
whenever you invoke orbit
on the container.
I’m wondering whether in some circumstances, like launching a coroutine or switching context to something other than unconfined in your orbit flow might cause the blocking constraint to be violated. My gut feeling is yes but I need a unit test for this corner case. Will continue in the evening.Oleksii Malovanyi
06/11/2021, 2:33 PMrunBlockingTest
could re-use TestCoroutineDispatcher
which could be passed through to the VM dependenciesMikolaj Leszczynski
06/11/2021, 2:35 PMblocking
flag not only changes the dispatchers, it also does this:
override fun orbit(orbitFlow: suspend ContainerContext<STATE, SIDE_EFFECT>.() -> Unit) {
if (!isolateFlow || dispatched.compareAndSet(0, 1)) {
if (blocking) {
runBlocking {
orbitFlow(pluginContext)
}
} else {
super.orbit(orbitFlow)
}
}
}
runBlocking
the test will block whatever you do inside your flow, not sure using just the TestCoroutineDuispatcher
will give you that guaranteeOleksii Malovanyi
06/11/2021, 2:36 PMMikolaj Leszczynski
06/11/2021, 2:37 PMrunBlocking
, I will re-verify with runBlockingTest
laterOleksii Malovanyi
06/11/2021, 2:38 PMrunBlockingTest(myTestCoroutineDispatcherInstance)
Mikolaj Leszczynski
06/16/2021, 6:38 AMrunBlockingTest(myTestDispatcher) {
val action = Random.nextInt()
val middleware = MyMiddleware().test(initialState)
middleware.doSomething(action)
container.assert(initialState) {
...
}
}
Oleksii Malovanyi
06/16/2021, 6:46 AMrunBlocking
-> as we swap the context completely from the container, the original Settings’ exceptionHandler no longer could be found in tests’s context: so what is working in production is not working in test mode…Mikolaj Leszczynski
06/16/2021, 6:51 AMintent
is not a suspending function - it triggers a coroutine launch using orbit’s internal scope so this method as it is right now will never block without some runBlocking
hidden in the test container. Writing tests to confirm.Oleksii Malovanyi
06/16/2021, 6:56 AMclass TestCoroutineDispatcherTest {
private val dispatcher = TestCoroutineDispatcher()
@Test
fun `on executor no delay`() = runBlockingTest(dispatcher) {
Executor(dispatcher).invoke()
}
}
class Executor(private val dispatcher: CoroutineDispatcher = Dispatchers.Default) {
suspend operator fun invoke() {
withContext(dispatcher) {
delay(10_000)
println("Yeah")
}
}
}
runBlockingTest
under the hood creates it’s own scope, but if we provide test dispatcher, it uses it to wait for all the coroutines started with it to finishMikolaj Leszczynski
06/16/2021, 7:00 AMOleksii Malovanyi
06/16/2021, 7:00 AMMikolaj Leszczynski
06/16/2021, 7:00 AMOleksii Malovanyi
06/16/2021, 7:03 AMassert
func use runBlockingTest under the hood? 🤔Mikolaj Leszczynski
06/16/2021, 7:04 AMOleksii Malovanyi
06/16/2021, 7:05 AMTestCoroutineDispatcher
is to deal with the delay
func to test the timeoutsrunBlocking
can’t rewind the time, so tests still pass but then the real time ticks..Mikolaj Leszczynski
06/16/2021, 7:07 AMkotlinx-coroutines-test
is not multiplatformOleksii Malovanyi
06/16/2021, 7:08 AMMikolaj Leszczynski
06/16/2021, 7:19 AMrunBlockingTest(myTestDispatcher)
as runBlockingTest
uses a TestCoroutineScope
internally which sets this dispatcher if one is not provided
public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
var safeContext = context
if (context[ContinuationInterceptor] == null) safeContext += TestCoroutineDispatcher()
if (context[CoroutineExceptionHandler] == null) safeContext += TestCoroutineExceptionHandler()
return TestCoroutineScopeImpl(safeContext)
}
Oleksii Malovanyi
06/16/2021, 7:58 AMMikolaj Leszczynski
06/16/2021, 7:59 AMTestCoroutineDispatcher
can have their scheduling controlled.
So, even if we set orbitDispatcher =TestCoroutineDispatcher
the orbit event loop substitutes it with Dispatchers.Unconfined
and advanceTimeBy
has no effect. We could let you replace the event loop dispatcher, but this doesn’t mean you can’t just withContext(Dispatchers.Default
in your flow and stop it working again…Oleksii Malovanyi
06/16/2021, 8:01 AMwithContext(Dispatchers.Default)
is the code smell for any coroutine-based code ¯\_(ツ)_/¯ till kotlin team doesn’t introduce the way to swap it like RxPlugins@OptIn(ExperimentalStdlibApi::class)
class TestCoroutineDispatcherTest {
@Test
fun `on executor no delay`() = runBlockingTest() {
val dispatcher = coroutineContext[CoroutineDispatcher]
Executor(dispatcher!!).invoke()
}
}
Mikolaj Leszczynski
06/16/2021, 8:07 AMOleksii Malovanyi
06/16/2021, 8:08 AMMikolaj Leszczynski
06/16/2021, 8:09 AMOleksii Malovanyi
06/16/2021, 8:10 AMMikolaj Leszczynski
06/16/2021, 8:18 AMtest-dispatcher-overrides
branch if you want to play around with it…
There are a couple of hacks still in there, very much a WIPOleksii Malovanyi
06/16/2021, 8:18 AMMikolaj Leszczynski
06/16/2021, 8:19 AMrunBlocking
that was there in TestContainer
, so I had to hack the assertions to await
, this is not exactly the experience I had in mindrunBlocking
in TestContainer and allowing you to override the dispatchersOleksii Malovanyi
06/16/2021, 8:20 AMMikolaj Leszczynski
06/16/2021, 8:20 AMOleksii Malovanyi
06/16/2021, 8:20 AMMikolaj Leszczynski
06/16/2021, 8:21 AMOleksii Malovanyi
06/16/2021, 12:22 PMMikolaj Leszczynski
06/16/2021, 12:25 PMUnconfined
is used on purpose here - it will execute on the orbit
dispatcher until the first suspension pointSettings
entryOleksii Malovanyi
06/16/2021, 12:28 PMStateTestMiddleware().test(initialState = initialState) {
somethingInBackground(action)
}.assert(initialState) {
states(
{ copy(count = action) }
)
}
Mikolaj Leszczynski
06/16/2021, 12:30 PMtoo fragile and implicit not to make a mistake in the future
- could you please elaborate?Oleksii Malovanyi
06/16/2021, 12:33 PMrunBlockingTest
block has to go always before the assert
call to guarantee any delays are rewinded (until idel) before we call the assertMikolaj Leszczynski
06/16/2021, 12:37 PMOleksii Malovanyi
06/16/2021, 12:38 PMMikolaj Leszczynski
06/16/2021, 12:39 PMrunBlockingTest under the hood (which I believe a good idea in terms of automagic solution)This is a no-go as long as coroutines test is not multiplatform 😞
Oleksii Malovanyi
06/16/2021, 12:39 PMbackgroundDispatcher
as long as it stays Unconfined
by default?Mikolaj Leszczynski
06/16/2021, 12:43 PMcomplex
syntaxOleksii Malovanyi
06/16/2021, 12:44 PMMikolaj Leszczynski
06/16/2021, 12:45 PMtest mode
setting in there tooOleksii Malovanyi
06/16/2021, 12:55 PMtest
method 🤔Mikolaj Leszczynski
06/16/2021, 12:58 PMOleksii Malovanyi
06/16/2021, 12:59 PMMikolaj Leszczynski
06/16/2021, 1:01 PMcoroutines-test
not being multiplatform is to use runBlockingTest
on jvm and runBlocking
everywhere elseOleksii Malovanyi
06/16/2021, 1:02 PMMikolaj Leszczynski
06/16/2021, 1:07 PMOleksii Malovanyi
06/16/2021, 1:08 PMMikolaj Leszczynski
06/16/2021, 1:09 PMContainer.orbit
as suspending…Oleksii Malovanyi
06/18/2021, 6:58 AMMikolaj Leszczynski
06/22/2021, 9:26 AMtest-overhaul
branch that you might want to look at.
TL;DR “blocking” test mode now completely circumvents orbit internal dispatching (which is well tested anyway).
Actually it’s not “blocking” at all any more - you invoke an intent
as a suspending function.
This means you can test it exactly as you would a normal suspending function e.g. using runBlockingTest
. Without any dispatcher related magic necessary.
Let me know what you think.Container.orbit
and send lambdas to a channel which then runs the suspending lambdas in a suspend functionintent
block.Oleksii Malovanyi
06/22/2021, 12:42 PMwithTimeout
call, but I realise it to be a defensive tactics we have to make due to async test/assert callsMikolaj Leszczynski
06/22/2021, 1:08 PMwithTimeout
is there just to guard against someone not actually calling any method on the containerHost in testIntent
.
This implementation is very rough around the edges right now anyway 🙂 There are a few issues that still need solving. I’ll keep working on this this week and tag you on the PR once I’m done.
What do you think about the general approach of using suspending functions for tests?testIntent
returns the test container host so you can chain it in a builder-like patternarrange/act/assert
but I don’t see why we can’t have both 😉Oleksii Malovanyi
06/22/2021, 1:13 PMMikolaj Leszczynski
06/22/2021, 1:16 PMOleksii Malovanyi
06/24/2021, 8:54 AMMikolaj Leszczynski
06/24/2021, 8:59 AM