Hello. I'm trying to understand a problem I found ...
# coroutines
a
Hello. I'm trying to understand a problem I found in a project I recently joined. On paper everything looks normal: • Kotlin app with coroutines (Kotlin 1.9.25, coroutines 1.8,1) •
kotlinx-coroutines-test
used in tests • JUnit
@Rule
with
Dispatchers.setMain
◦ with a
UnconfinedTestDispatcher()
in its constructor In practice, something strange happens It turns out that during the construction of
UnconfinedTestDispatcher
there is a call to
Dispatchers.getMain
(!), which is invoked before we get a chance to call
setMain
(because we're only trying to construct a dispatcher first) The
getMain
invocation goes to
kotlinx-coroutines-android
which is not needed in unit tests but we get it transitively e.g. via
org.jetbrains.kotlinx:kotlinx-coroutines-bom
that is included by many libraries such as
implementation "androidx.lifecycle:lifecycle-runtime"
so we accidentally get it in our test classpath which then tries to
Looper.getMainLooper()
, and that obviously fails This is the callstack (more or less): 1.
UnconfinedTestDispatcher
2.
TestMainDispatcher.currentTestScheduler
3.
TestMainDispatcher.currentTestDispatcher
4.
Dispatchers.Main
(getter) 5.
MainDispatcherLoader.loadMainDispatcher
6.
MainDispatchersKt.tryCreateDispatcher
7.
kotlinx.coroutines.android.AndroidDispatcherFactory.createDispatcher
8.
Looper.getMainLooper()
If I get rid of
kotlinx-coroutines-android
dependency by hand then the unit tests work fine
Copy code
configurations.all {
    exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-android")
}
But I've never had to do that in any other project I participated in so I presume it's not the intended way to solve this problem Has anyone seen a similar issue?
d
If you replace
UnconfinedTestDispatcher()
with
UnconfinedTestDispatcher(TestCoroutineScheduler())
, does the issue persist?
a
No, it goes away as expected because the execution path is slightly different. I should have mentioned it as well, sorry for that. Anyway, it feels strange because I've never had to do that in other projects so I'm worried that I missed something important.
Could this be caused by a version mismatch between coroutines-core and coroutines-test?
I've just added an explicit dependency on coroutines-core (I guess we've got it transitively earlier, but maybe with a different version) and it also seems to solve the problem 🤔
I checked
Copy code
dependencyInsight --configuration debugUnitTestCompileClasspath --dependency org.jetbrains.kotlinx:kotlinx-coroutines-core
before and after adding the explicit
org.jetbrains.kotlinx:kotlinx-coroutines-core
dependency and I can't see significant changes in the output so, to be honest, I'm not sure why adding it also seems to solve the problem
(did the same for runtime classpath and also no major changes)
d
org.jetbrains.kotlinx:kotlinx-coroutines-bom
that is included by many libraries such as
implementation "androidx.lifecycle:lifecycle-runtime"
so we accidentally get it in our test classpath
This shouldn't be the case, because
-bom
ensures that all dependencies that are present anyway have the same version (see https://docs.gradle.org/current/userguide/platforms.html#sec:regular-platform). It shouldn't add all dependencies on its own. Something else must be adding
kotlinx-coroutines-android
. On the coroutines side, the code does say that the main dispatcher gets initialized when you call
UnconfinedTestDispatcher()
, but it's actually not necessary. If it's an issue, I can fix this in the next release, and the Android dispatcher is not going to get created.
so, to be honest, I'm not sure why adding it also seems to solve the problem
Yeah, me neither. Maybe it's not a clean build and something got incorrectly cached?
👍 1
a
Maybe it's not a clean build and something got incorrectly cached?
Yes, it's not a clean build, but I'm switching between these two states (with and without the explicit dependency) and I get consistent results (success and failures) so I wouldn't blame cache for that unless I'm being too naive 🤔
This shouldn't be the case, because
-bom
ensures that all dependencies that are present anyway have the same version
Thanks for explaining the thing with BOM. Actually, that's how I expected it to work. There were more dependencies and I copy-pasted the first I noticed without thinking too much about it. Looks like another dependency must have included coroutines-android more directly.
If it's an issue, I can fix this in the next release, and the Android dispatcher is not going to get created.
If initializing the main dispatcher is not necessary for constructing the test dispatcher, then maybe it would be better
It's just very confusing when you try to create a dispatcher to call
setMain
and you are told that the problem is ... you didn't call
setMain
🤯
d
If initializing the main dispatcher is not necessary for constructing the test dispatcher, then maybe it would be better
Ok, do you want to file the issue (https://github.com/Kotlin/kotlinx.coroutines/issues), or should I do it?
a
I will 👌
🙏 1
Correction: none of the workarounds I mentioned earlier seem to work after all. Since I was trying to fix multiple failing unit tests, I probably didn't notice that whenever I applied a workaround (such as an additional explicit dependency or an exclusion of coroutines-android, or calling
UnconfinedTestDispatcher(TestCoroutineScheduler())
), one test was getting "fixed" but another one contained a suppressed exception in its stacktrace pointing to the same kind of problem.
That being said, I'm no longer able to tell how to resolve or circumvent this problem and I'm still seeking for a solution.
Also, I'm considering closing the GitHub issue I created because I no longer feel like I know what I'm talking about.
I'm also starting to believe that this project has some other kinds of issues (e.g. one test affects other tests somehow).