Hello folks, I've been trying to migrate from runB...
# coroutines
c
Hello folks, I've been trying to migrate from runBlocking to runTest but to no avail. More details inside the thread. Thank you
solved 1
🧵 2
m
Yeap, it sounds like you are still running multiple threads in your tests. Usually happens when I forget to inject the same test dispatcher everywhere. Here it is how it works for me: https://github.com/MartinRajniak/CatViewerDemo/blob/main/shared/src/commonTest/kotlin/eu/rajniak/cat/CatsTest.kt ( I only skimmed your code but I don't see where you are using your DispatcherProvider - I would expect to see it used with your
runInBackgroud
extension )
c
Gotcha. Yeah, I was using it. I was injecting it inside my ViewModel and then passing it to the method you mentioned. So when I ran the test, a TestDispatcher would be used but even after that, it didn't work. I even tried to pass the scope of runTest to the method but still with no success. It's a mystery to me really haha
I reverted the code to have dispatcher injection again. Maybe people find it easier to understand but I think the problem might be on something else because the test keeps failing. But thanks for your help, Martin 👍
n
Please keep posts short and explain further in threads. Try calling
runCurrent()
before you verify, assuming your background code is using the test dispatcher.
c
I'm gonna put all info here since people have asked me to put this inside a thread: I have already tried to inject both the scope and dispatcher(StandardTestDispatcher and UnconfinedTestDispatcher) according to this article(https://craigrussell.io/2021/12/testing-android-coroutines-using-runtest/) but that did not work either so I reverted back to what it was. What I noticed is that even though I'm using runTest that is supposed to run tests in a blocking way given it's a replacement for runBlockingTest, it skips straight through to the assertion and of course, if the task being run takes longer than expected, the assertion will fail because the asserted value at the end hasn't been modified yet. The point is that if I put a breakpoint on line 80 of the test class(https://github.com/caiodev/Weather/blob/e3e448e0f538be652b41c88e965932334b24a454/a[…]roidTest/java/com/project/weatherreport/WeatherViewModelTest.kt) that's the line which invokes the suspended task, the test passes because the debugger will wait for the result and I can even inspect the result which I confirmed is according to what is expected. Have you guys run into this problem already? Thanks in advance 👍 This is the class I'm testing: https://github.com/caiodev/Weather/blob/master/app/src/main/java/com/project/weatherreport/weather/viewModel/WeatherViewModel.kt This is the test class: https://github.com/caiodev/Weather/blob/master/app/src/androidTest/java/com/project/weatherreport/WeatherViewModelTest.kt And this is the CoroutinesRule I'm using: https://github.com/caiodev/Weather/blob/master/app/src/androidTest/java/com/project/weatherreport/api/coroutines/CoroutineTestRule.kt Kotlin version is 1.6.10 Coroutines version is: 1.6.0 All I get is this error:
Copy code
java.lang.AssertionError: expected:<Toronto> but was:<null>
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.failNotEquals(Assert.java:835)
at org.junit.Assert.assertEquals(Assert.java:120)
at org.junit.Assert.assertEquals(Assert.java:146)
at com.project.weatherreport.WeatherViewModelTest$checkIfASuccessfulCallAlreadyBeenMade$1.invokeSuspend(WeatherViewModelTest.kt:79)
at com.project.weatherreport.WeatherViewModelTest$checkIfASuccessfulCallAlreadyBeenMade$1.invoke(WeatherViewModelTest.kt)
at com.project.weatherreport.WeatherViewModelTest$checkIfASuccessfulCallAlreadyBeenMade$1.invoke(WeatherViewModelTest.kt)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$2.invokeSuspend(TestBuilders.kt:208)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$2.invoke(TestBuilders.kt)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$2.invoke(TestBuilders.kt)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:55)
at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:112)
at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:126)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:207)
at kotlinx.coroutines.test.TestBuildersKt.runTestCoroutine(Unknown Source)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invokeSuspend(TestBuilders.kt:167)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
at kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:13)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at kotlinx.coroutines.test.TestBuildersJvmKt.createTestResult(TestBuildersJvm.kt:12)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:166)
at kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:154)
at kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest$default(TestBuilders.kt:147)
at kotlinx.coroutines.test.TestBuildersKt.runTest$default(Unknown Source)
at com.project.weatherreport.WeatherViewModelTest.checkIfASuccessfulCallAlreadyBeenMade(WeatherViewModelTest.kt:72)
My bad, @Nick Allen
I updated the code, @Nick Allen, tried to run it again, same outcome.
Thanks for the tip though 👍
n
How about:
Copy code
viewModel.viewModelScope.coroutineContext.job.children.forEach { it.join() }
solved 1
c
Dude, tysm, @Nick Allen. It worked like a charm. I took at look at the join() method that's being called inhside this forEach and I kind of got what it is doing. It suspends the parent coroutine until all jobs are completed is that it? Please, correct me if I'm wrong here haha. It's so good to see it all green 😄
n
A suggestion: modify
runTaskOnBackground
and
getWeatherInfo
to return the
Job
from
launch
and then
join
on just that in the test.
c
Copy code
A suggestion: modify runTaskOnBackground and getWeatherInfo to return the Job from launch and then join on just that in the test.
Gotcha, Tks 👍
e
Thank you people, this thread helped me to migrate also
However, I have still couple of tests failing for various reasons
Looks like some mockito checks of the suspend functions is not working with new coroutines
Interesting, I started adding
Copy code
testScheduler.advanceUntilIdle()
and tests becomes green
j
If you migrated from
runBlockingTest
to
runTest
, keep in mind that
advanceTimeBy
changed semantics, and now it doesn't run the tasks that are exactly scheduled at the final time anymore. You should add
runCurrent()
after
advanceTimeBy
if you want the former behaviour. Using
advanceUntilIdle
also works, but it's not equivalent (it will run also later tasks as long as some are scheduled).
m
@Nick Allen @Caio Costa this seems like a hack 🙂 IMHO, you shouldn't need to expose job from the coroutine just to join it in tests. Injecting
TestScope
should work.
@Eugen Martynov Could you share the code (and maybe let's use different thread so we doesn't mix conversations 🙂)
c
Copy code
Injecting TestScope should work.
Indeed. I'd rather not to expose Job but I've tried almost anything you can imagine including injecting both scopes and dispatchers and the only thing that made my test pass was expose the Coroutine Job
Hopefully in the future I won't need it anymore
m
My bad - you are using
LiveData
- I am used to using
StateFlow
now. IMHO, this problem is this one (nice write-up): https://medium.com/androiddevelopers/unit-testing-livedata-and-other-common-observability-problems-bb477262eb04 Based on the article - this solution works: https://github.com/caiodev/Weather/pull/1
(BTW, why are you not testing UI directly with espresso / compose or alternatively test the app in JVM - outside of
androidTest
)
c
Yeah. This project is a prototype so things are looking ugly atm but I'll improve it overtime. I was just experimenting things to implement on my main project which is the GithubProfileSearcher. There I'm already using StateFlow and all that good stuff but I wanted to test those changes in a less complex project first. It's interesting that I replaced Livedata for StateFlow a couple of days ago but got the same error. But I'll take a look at your solution, Martin. Tysm for the time you took to submit that PR 👍
🤘 1
n
Took another look. The problematic threading is in the RetrofitTestService. It builds a Retrofit instance that uses its own background executor that is not tied to the test dispatcher at all. If you added
callbackExecutor { it.run() }
(executor that runs in place) to the Retrofit builder then I think that would remove the issue.
And I'd suggest just mocking the interface with mockk instead of using retrofit's helper. Then you wouldn't have run into this in the first place.
c
Is there an equivalent of getOrAwaitValue for StateFlows, @Martin Rajniak? I think that would solve the problem when I migrate. Because I'm testing to get the value like this: assertEquals("Toronto", viewModel.successStateFlow.value.title), but I get the same error I got earlier without the Util method for LiveDatas
I tried changing the class you mentioned, @Nick Allen, to callbackExecutor { it.run() } but I still get the error if I remove the join mathod. I don't know if I got what you tried to suggest. Could you show a code example?
m