https://kotlinlang.org logo
#android
Title
# android
d

dave08

03/01/2020, 5:53 PM
How do you test Android ViewModels that use a viewModelScope to launch a coroutine and post a result to LiveData...? Even with `
Copy code
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
and
Copy code
shadowOf(Looper.getMainLooper()).idle()
when I try
Copy code
someLiveData.asFlow().first()
it just stays stuck with runBlocking and crashes with runBlockingTest...
a

Anastasia Finogenova

03/01/2020, 6:45 PM
Watch

https://youtu.be/KMb0Fs8rCRs

I didn't find any issues with viewmodel scope itself but had to change the implementation to inject the dispatcher into the viewmodel constructor to replace it while testing for test coroutine dispatcher
Also runBlockingTest seems to be broken for some cases so had to use runBlocking
a

Ahmed Ibrahim

03/01/2020, 9:41 PM
You need to use
Dispatchers.setMain
available in
kotlinx-coroutines-test
lib. That way you override Dispatchers.Main which the viewModelScope uses. You can take a look there and I think you'll get what I mean. https://gist.github.com/manuelvicnt/049ce057fa6b5e5c785ec9fff7c22a7c
a

Anastasia Finogenova

03/01/2020, 10:02 PM
The method above will only work if you execute the coroutine is the Dispatchers.Main
g

ghedeon

03/02/2020, 12:40 AM
usually there is no need to inject dispatchers, it's not RxJava.
setMain()
and
runBlocking()
is enough to synchronize the execution.
a

Anastasia Finogenova

03/02/2020, 1:26 AM
Setting main is of no use if you are using a custom dispatcher or for example Dispatchers.io (default dispatchers), if you watch the talk I posted the link to , Google recommends injecting for testability and for RxJava you don't need to inject as you can replace any default dispatcher with RxAndroidPlugins in tests but for coroutines you can only do setMain(). There is no setIO for example. I believe there is an open request to add those as well in the repo
r

rkeazor

03/02/2020, 6:28 AM
You use a testDispatcher.
1
g

ghedeon

03/02/2020, 7:13 AM
@Anastasia Finogenova you don't need to replace Dispatchers.IO in tests, runBlockingTest will synchronize it. That's the beauty of structured concurrency, just try it 😏. Maybe in that video they account for the case where something is done in a separate coroutine.
d

dave08

03/02/2020, 11:59 AM
Thanks a lot for all your comments 😉, they helped a lot!
Ok, it still seems to be hanging on this code:
Copy code
suspend fun <A : Activity> AccountManager.addAccount(activity: A?, accountType: String, authTokenType: String, options: Bundle? = null) =
    callAsync<Bundle> { callback -> addAccount(accountType, authTokenType, null, options, activity, callback, null) }


/**
 * Callback class that uses a continuation as the callback for the account manager. Note that
 * this callback is NOT designed to survive the destruction of the [Context] ([Activity]).
 *
 * @property cont The continuation that will be invoked on completion.
 */
class CoroutineAccountManagerCallback<T>(private val cont: CancellableContinuation<T>) : AccountManagerCallback<T> {
    @UseExperimental(InternalCoroutinesApi::class)
    override fun run(future: AccountManagerFuture<T>) {
        try {
            if (future.isCancelled) {
                cont.cancel()
            } else {
                cont.resume(future.result)
            }
        } catch (e: Exception) {
            if (e is CancellationException) {
                cont.cancel(e)
            } else {
                cont.tryResumeWithException(e)
            }
        }
    }
}

/**
 * Helper function that helps with calling account manager operations asynchronously.
 *
 * @receiver The account manager. This is actually not stable.
 */
@Deprecated("Use a special ContextCoroutine that doesn't put a context in the capture", ReplaceWith("callAccountManagerAsync(context, operation)"))
suspend inline fun <R> AccountManager.callAsync(crossinline operation: AccountManager?.(CoroutineAccountManagerCallback<R>) -> Unit): R {
    return suspendCancellableCoroutine<R> { cont ->
        operation(CoroutineAccountManagerCallback(cont))
    }
}
I'm using the CoroutinesTestRule above, and shadowMainLooper.idle()... maybe the above code is wrong (or not compatible with Robolectric's ShadowAccountManager? I took it from a Github repo somewhere...
a

Anastasia Finogenova

03/02/2020, 5:32 PM
@rkeazor i started with just having runBlockingTest will only sync the coroutines within one dispatcher. If you start it without the dispatcher it will pick up the default dispatcher. the issue link clearly describes that the only way as of right now is to inject and replace in the test
👍 1
❤️ 1
89 Views