Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Coroutines - unit testing viewModelScope.launch methods

Tags:

I am writing unit tests for my viewModel, but having trouble executing the tests. The runBlocking { ... } block doesn't actually wait for the code inside to finish, which is surprising to me.

The test fails because result is null. Why doesn't runBlocking { ... } run the launch block inside the ViewModel in blocking fashion?

I know if I convert it to a async method that returns a Deferred object, then I can get the object by calling await(), or I can return a Job and call join(). But, I'd like to do this by leaving my ViewModel methods as void functions, is there a way to do this?

// MyViewModel.kt  class MyViewModel(application: Application) : AndroidViewModel(application) {      val logic = Logic()     val myLiveData = MutableLiveData<Result>()      fun doSomething() {         viewModelScope.launch(MyDispatchers.Background) {             System.out.println("Calling work")             val result = logic.doWork()             System.out.println("Got result")             myLiveData.postValue(result)             System.out.println("Posted result")         }     }      private class Logic {         suspend fun doWork(): Result? {           return suspendCoroutine { cont ->               Network.getResultAsync(object : Callback<Result> {                       override fun onSuccess(result: Result) {                           cont.resume(result)                       }                       override fun onError(error: Throwable) {                           cont.resumeWithException(error)                       }                   })           }     } } 
// MyViewModelTest.kt  @RunWith(RobolectricTestRunner::class) class MyViewModelTest {      lateinit var viewModel: MyViewModel      @get:Rule     val rule: TestRule = InstantTaskExecutorRule()      @Before     fun init() {         viewModel = MyViewModel(ApplicationProvider.getApplicationContext())     }      @Test     fun testSomething() {         runBlocking {             System.out.println("Called doSomething")             viewModel.doSomething()         }         System.out.println("Getting result value")         val result = viewModel.myLiveData.value         System.out.println("Result value : $result")         assertNotNull(result) // Fails here     } }  
like image 866
Prem Avatar asked Apr 19 '19 16:04

Prem


People also ask

How do you use a ViewModelScope in coroutines?

Add a CoroutineScope to your ViewModel by creating a new scope with a SupervisorJob that you cancel in the onCleared() method. The coroutines created with that scope will live as long as the ViewModel is being used.

What is coroutines launch?

Launching a Coroutine as a JobThe launch {} function returns a Job object, on which we can block until all the instructions within the coroutine are complete, or else they throw an exception. The actual execution of a coroutine can also be postponed until we need it with a start argument. If we use CoroutineStart.

What is the ViewModelScope?

A ViewModelScope is defined for each ViewModel in your app. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared. Coroutines are useful here for when you have work that needs to be done only if the ViewModel is active.

How do you test coroutines Kotlin?

To test code that includes suspend functions, you need to do the following: Add the kotlinx-coroutines-test test dependency to your app's build. gradle file. Annotate the test class or test function with @ExperimentalCoroutinesApi .


1 Answers

What you need to do is wrap your launching of a coroutine into a block with given dispatcher.

var ui: CoroutineDispatcher = Dispatchers.Main var io: CoroutineDispatcher =  Dispatchers.IO var background: CoroutineDispatcher = Dispatchers.Default  fun ViewModel.uiJob(block: suspend CoroutineScope.() -> Unit): Job {     return viewModelScope.launch(ui) {         block()     } }  fun ViewModel.ioJob(block: suspend CoroutineScope.() -> Unit): Job {     return viewModelScope.launch(io) {         block()     } }  fun ViewModel.backgroundJob(block: suspend CoroutineScope.() -> Unit): Job {     return viewModelScope.launch(background) {         block()     } } 

Notice ui, io and background at the top. Everything here is top-level + extension functions.

Then in viewModel you start your coroutine like this:

uiJob {     when (val result = fetchRubyContributorsUseCase.execute()) {     // ... handle result of suspend fun execute() here          } 

And in test you need to call this method in @Before block:

@ExperimentalCoroutinesApi private fun unconfinifyTestScope() {     ui = Dispatchers.Unconfined     io = Dispatchers.Unconfined     background = Dispatchers.Unconfined } 

(Which is much nicer to add to some base class like BaseViewModelTest)

like image 65
Stanislav Kinzl Avatar answered Sep 21 '22 15:09

Stanislav Kinzl