Search code examples
androidunit-testingkotlinkotlinx.coroutinesandroid-viewmodel

Best approach for unit-testing scoped viewmodels


When dealing with coroutines inside a viewModel is best to have said viewModel implement CoroutineScope so all coroutines are cancelled when the viewModel is cleared. Usually I see coroutineContext defined as Dispatchers.Main + _job so that coroutines are executed in the main UI thread by default. Usually this is done on a open class so that all your viewModels can extend it and get the scope without boilerplate code.

The issue arises when trying to unit test said viewModels as Dispatchers.Main is not available and trying to use it throws an exception. I am tryin to find a good solution that doesn't involve external libraries or too much boiler plate on the child viewModels.

My current solution is to add the maincontext as a contructor paramenter with the Dispatchers.Main as the default value. Then in the unit test, before testing the viewModel I set it to Dispatchers.Default. I don't quiet like this solution as it exposes the coroutineContext implementation details for everyone to see and change:

open class ScopedViewModel(var maincontext = Dispatchers.Main) : ViewModel(), CoroutineScope {
    private val _job = Job()
    override val coroutineContext: CoroutineContext
        get() = maincontext + _job

    override fun onCleared() {
        super.onCleared()
        _job.cancel()
    }
}
class MyViewModel : ScopedViewModel() {}

In the tests:

fun setup(){
    viewModel = MyViewModel()
    viewModel.maincontext = Dispacther.Default
}

Solution

  • Personally I copied a solution from RxJava2: if your test runs against RxJava2 flow which includes two or more different schedulers, you want, sure, all of them to actually run in a single thread. Here is how it is done with RxJava2 testing:

    @BeforeClass
    public static void prepare() {
        RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
        RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
        RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
        RxJavaPlugins.setSingleSchedulerHandler(scheduler -> Schedulers.trampoline());
        RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
    }
    

    I did the same for coroutines. Just have created a class which collects dispatchers, but these dispatchers can be changed.

    object ConfigurableDispatchers {
    
    @JvmStatic
    @Volatile
    var Default: CoroutineDispatcher = Dispatchers.Default
    
    @JvmStatic
    @Volatile
    var Main: MainCoroutineDispatcher = Dispatchers.Main
    
    ...
    }
    

    And, inside @BeforeClass method I call

    @ExperimentalCoroutinesApi
    fun setInstantMainDispatcher() {
        Main = object : MainCoroutineDispatcher() {
            @ExperimentalCoroutinesApi
            override val immediate: MainCoroutineDispatcher
                get() = this
    
            override fun dispatch(context: CoroutineContext, block: Runnable) {
                block.run()
            }
        }
    }
    

    That will guarantee that the block will be executed in the calling thread.

    It is the only alternative I found to constructor injection.