How do you test Android ViewModels that use a view...
# android
d
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
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
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
The method above will only work if you execute the coroutine is the Dispatchers.Main
g
usually there is no need to inject dispatchers, it's not RxJava.
setMain()
and
runBlocking()
is enough to synchronize the execution.
a
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
You use a testDispatcher.
1
g
@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
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
@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
111 Views