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 😞Oleksii Malovanyi
06/10/2021, 4:14 PMOleksii Malovanyi
06/10/2021, 6:22 PMblocking param from the test() extensions and swap it with the dispatcher (unconfined) by default -> then it would be easy to use own TestCoroutineDispatcherMikolaj Leszczynski
06/11/2021, 7:25 AMMikolaj Leszczynski
06/11/2021, 7:25 AMMikolaj Leszczynski
06/11/2021, 7:26 AMMikolaj Leszczynski
06/11/2021, 2:30 PMblocking 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)
}
}
}Mikolaj Leszczynski
06/11/2021, 2:36 PMrunBlocking 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 PMOleksii Malovanyi
06/11/2021, 2:37 PMMikolaj Leszczynski
06/11/2021, 2:37 PMMikolaj Leszczynski
06/11/2021, 2:37 PMrunBlocking , I will re-verify with runBlockingTest laterMikolaj Leszczynski
06/11/2021, 2:38 PMOleksii 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 AMOleksii Malovanyi
06/16/2021, 6:50 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 AMMikolaj Leszczynski
06/16/2021, 6:53 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")
}
}
}Oleksii Malovanyi
06/16/2021, 6:58 AMrunBlockingTest
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 AMMikolaj Leszczynski
06/16/2021, 7:01 AMMikolaj Leszczynski
06/16/2021, 7:02 AMOleksii Malovanyi
06/16/2021, 7:03 AMassert func use runBlockingTest under the hood? 🤔Mikolaj Leszczynski
06/16/2021, 7:04 AMMikolaj Leszczynski
06/16/2021, 7:05 AMMikolaj Leszczynski
06/16/2021, 7:05 AMOleksii Malovanyi
06/16/2021, 7:05 AMTestCoroutineDispatcher is to deal with the delay func to test the timeoutsOleksii Malovanyi
06/16/2021, 7:06 AMrunBlocking can’t rewind the time, so tests still pass but then the real time ticks..Mikolaj Leszczynski
06/16/2021, 7:07 AMMikolaj Leszczynski
06/16/2021, 7:07 AMMikolaj Leszczynski
06/16/2021, 7:08 AMkotlinx-coroutines-test is not multiplatformOleksii Malovanyi
06/16/2021, 7:08 AMOleksii Malovanyi
06/16/2021, 7:14 AMMikolaj Leszczynski
06/16/2021, 7:19 AMMikolaj Leszczynski
06/16/2021, 7:29 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 AMOleksii Malovanyi
06/16/2021, 7:59 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 RxPluginsOleksii Malovanyi
06/16/2021, 8:01 AMOleksii Malovanyi
06/16/2021, 8:03 AM@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 AMMikolaj Leszczynski
06/16/2021, 8:08 AMOleksii Malovanyi
06/16/2021, 8:08 AMMikolaj Leszczynski
06/16/2021, 8:09 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 WIPMikolaj Leszczynski
06/16/2021, 8:18 AMOleksii 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 mindMikolaj Leszczynski
06/16/2021, 8:19 AMrunBlocking in TestContainer and allowing you to override the dispatchersMikolaj Leszczynski
06/16/2021, 8:20 AMOleksii 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 AMMikolaj Leszczynski
06/16/2021, 8:21 AMMikolaj Leszczynski
06/16/2021, 8:21 AMOleksii Malovanyi
06/16/2021, 12:22 PMMikolaj Leszczynski
06/16/2021, 12:25 PMMikolaj Leszczynski
06/16/2021, 12:27 PMUnconfined is used on purpose here - it will execute on the orbit dispatcher until the first suspension pointMikolaj Leszczynski
06/16/2021, 12:27 PMSettings 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 assertOleksii Malovanyi
06/16/2021, 12:35 PMMikolaj Leszczynski
06/16/2021, 12:37 PMMikolaj Leszczynski
06/16/2021, 12:38 PMMikolaj Leszczynski
06/16/2021, 12:38 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 PMOleksii Malovanyi
06/16/2021, 12:40 PMOleksii Malovanyi
06/16/2021, 12:41 PMbackgroundDispatcher as long as it stays Unconfined by default?Oleksii Malovanyi
06/16/2021, 12:42 PMOleksii Malovanyi
06/16/2021, 12:43 PMMikolaj Leszczynski
06/16/2021, 12:43 PMMikolaj Leszczynski
06/16/2021, 12:43 PMMikolaj Leszczynski
06/16/2021, 12:43 PMcomplex syntaxMikolaj Leszczynski
06/16/2021, 12:44 PMMikolaj Leszczynski
06/16/2021, 12:44 PMMikolaj Leszczynski
06/16/2021, 12:44 PMOleksii Malovanyi
06/16/2021, 12:44 PMOleksii Malovanyi
06/16/2021, 12:45 PMMikolaj Leszczynski
06/16/2021, 12:45 PMMikolaj Leszczynski
06/16/2021, 12:51 PMMikolaj Leszczynski
06/16/2021, 12:51 PMMikolaj Leszczynski
06/16/2021, 12:52 PMtest mode setting in there tooMikolaj Leszczynski
06/16/2021, 12:52 PMOleksii Malovanyi
06/16/2021, 12:55 PMOleksii Malovanyi
06/16/2021, 12:56 PMtest method 🤔Mikolaj Leszczynski
06/16/2021, 12:58 PMMikolaj Leszczynski
06/16/2021, 12:58 PMOleksii Malovanyi
06/16/2021, 12:59 PMMikolaj Leszczynski
06/16/2021, 1:01 PMMikolaj Leszczynski
06/16/2021, 1:02 PMcoroutines-test not being multiplatform is to use runBlockingTest on jvm and runBlocking everywhere elseOleksii Malovanyi
06/16/2021, 1:02 PMOleksii Malovanyi
06/16/2021, 1:03 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…Mikolaj Leszczynski
06/16/2021, 1:09 PMMikolaj Leszczynski
06/16/2021, 1:10 PMMikolaj Leszczynski
06/16/2021, 1:10 PMMikolaj Leszczynski
06/17/2021, 8:51 PMOleksii 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.Mikolaj Leszczynski
06/22/2021, 9:27 AMContainer.orbit and send lambdas to a channel which then runs the suspending lambdas in a suspend functionMikolaj Leszczynski
06/22/2021, 9:28 AMMikolaj Leszczynski
06/22/2021, 9:32 AMintent 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?Mikolaj Leszczynski
06/22/2021, 1:09 PMtestIntent returns the test container host so you can chain it in a builder-like patternMikolaj Leszczynski
06/22/2021, 1:09 PMarrange/act/assert but I don’t see why we can’t have both 😉Oleksii Malovanyi
06/22/2021, 1:13 PMOleksii Malovanyi
06/22/2021, 1:14 PMMikolaj Leszczynski
06/22/2021, 1:16 PMMikolaj Leszczynski
06/22/2021, 1:17 PMMikolaj Leszczynski
06/24/2021, 8:07 AMMikolaj Leszczynski
06/24/2021, 8:08 AMOleksii Malovanyi
06/24/2021, 8:54 AMMikolaj Leszczynski
06/24/2021, 8:59 AMMikolaj Leszczynski
06/24/2021, 8:59 AM