Coroutines - unit testing viewModelScope.launch methods
Asked Answered
T

8

34

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
    }
}

Tojo answered 19/4, 2019 at 16:56 Comment(3)
Please ensure that your question shows a Minimal, Complete, and Verifiable example (stackoverflow.com/help/mcve), it would make it easier to answer your question.Unoccupied
runBlocking will only wait for child coroutines. Coroutines created using viewModelScope is not related to the scope inside runBlocking.Castrato
Better approach is to pass dispatcher context to viewmodel, so you can pass your test dispather.. in your tests!Kostival
A
5

As others mentioned, runblocking just blocks the coroutines launched in it's scope, it's separate from your viewModelScope. What you could do is to inject your MyDispatchers.Background and set the mainDispatcher to use dispatchers.unconfined.

Amy answered 17/6, 2019 at 13:47 Comment(2)
Thanks @Gergely Hegedus. Craig Russell outlines a similar strategy. Do you have any simple samples to show how to organize the production/test-code?Pony
Here is a strategy for injecting viewModelScope into the ViewModel: How to inject viewModelScope for Android unit test with Kotlin coroutines?.Pony
V
5

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)

Vociferance answered 15/7, 2019 at 14:46 Comment(3)
You are not allowed to use ui dispatcher inside the viewmodel scope Nothing related to the UI should be inside the ViewModel Read more how to use ViewModelsBlunder
The default dispatcher of the viewModeScope is already the Main Dispatcher @EdgarKhimichPoacher
We should be using the viewModelScope's default Main.immediate dispatcher and passing the background thread tasks to the use cases. This way its very easy to test. @EdgarKhimich we need to set the livedata.value using ui/main thread.Miquelmiquela
P
4

As @Gergely Hegedus mentions above, the CoroutineScope needs to be injected into the ViewModel. Using this strategy, the CoroutineScope is passed as an argument with a default null value for production. For unit tests the TestCoroutineScope will be used.

SomeUtils.kt

/**
 * Configure CoroutineScope injection for production and testing.
 *
 * @receiver ViewModel provides viewModelScope for production
 * @param coroutineScope null for production, injects TestCoroutineScope for unit tests
 * @return CoroutineScope to launch coroutines on
 */
fun ViewModel.getViewModelScope(coroutineScope: CoroutineScope?) =
    if (coroutineScope == null) this.viewModelScope
    else coroutineScope

SomeViewModel.kt

class FeedViewModel(
    private val coroutineScopeProvider: CoroutineScope? = null,
    private val repository: FeedRepository
) : ViewModel() {

    private val coroutineScope = getViewModelScope(coroutineScopeProvider)

    fun getSomeData() {
        repository.getSomeDataRequest().onEach {
            // Some code here.            
        }.launchIn(coroutineScope)
    }

}

SomeTest.kt

@ExperimentalCoroutinesApi
class FeedTest : BeforeAllCallback, AfterAllCallback {

    private val testDispatcher = TestCoroutineDispatcher()
    private val testScope = TestCoroutineScope(testDispatcher)
    private val repository = mockkClass(FeedRepository::class)
    private var loadNetworkIntent = MutableStateFlow<LoadNetworkIntent?>(null)

    override fun beforeAll(context: ExtensionContext?) {
        // Set Coroutine Dispatcher.
        Dispatchers.setMain(testDispatcher)
    }

    override fun afterAll(context: ExtensionContext?) {
        Dispatchers.resetMain()
        // Reset Coroutine Dispatcher and Scope.
        testDispatcher.cleanupTestCoroutines()
        testScope.cleanupTestCoroutines()
    }

    @Test
    fun topCafesPoc() = testDispatcher.runBlockingTest {
        ...
        val viewModel = FeedViewModel(testScope, repository)
        viewmodel.getSomeData()
        ...
    }
}
Pony answered 13/6, 2020 at 1:47 Comment(0)
D
0

I tried the top answer and worked, but I didn't want to go over all my launches and add a dispatcher reference to main or unconfined in my tests. So I ended up adding this code to my base testing class. I am defining my dispatcher as TestCoroutineDispatcher()

class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
    private val mainThreadDispatcher = TestCoroutineDispatcher()

    override fun beforeEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance()
            .setDelegate(object : TaskExecutor() {
                override fun executeOnDiskIO(runnable: Runnable) = runnable.run()

                override fun postToMainThread(runnable: Runnable) = runnable.run()

                override fun isMainThread(): Boolean = true
            })

        Dispatchers.setMain(mainThreadDispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance().setDelegate(null)
        Dispatchers.resetMain()
    }
}

in my base test class I have

@ExtendWith(MockitoExtension::class, InstantExecutorExtension::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class BaseTest {

    @BeforeAll
    private fun doOnBeforeAll() {
        MockitoAnnotations.initMocks(this)
    }
}
Diaghilev answered 27/2, 2020 at 16:24 Comment(2)
Don’t use Dispatchers.Unconfined as a replacement of Dispatchers.Main, it will break all assumptions and timings for code that does use Dispatchers.Main. Since a unit test should run well in isolation and without any side effects, you should call Dispatchers.resetMain() and clean up the executor when the test finishes running. reference: medium.com/androiddevelopers/… .. heading: Unit Testing viewModelScope 4th paragraph...Oregon
thanks for the info. I am editing my answer as I found TestCoroutineDispatcher a better fit, and I am also applying rest after each test. I remember reading over that same blog you posted but it's kind of old now..Diaghilev
D
0

I did use the mockk framework that helps to mock the viewModelScope instance like below

https://mockk.io/

viewModel = mockk<MyViewModel>(relaxed = true)
every { viewModel.viewModelScope}.returns(CoroutineScope(Dispatchers.Main))
Date answered 11/2, 2022 at 16:26 Comment(0)
N
0

There are 3 steps that you need to follow.

  1. Add dependency in gradle file.
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1") 
{ exclude ("org.jetbrains.kotlinx:kotlinx-coroutines-debug") }
  1. Create a Rule class MainCoroutineRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@ExperimentalCoroutinesApi
class MainCoroutineRule(private val testDispatcher: TestDispatcher = StandardTestDispatcher()) :
    TestWatcher() {

    override fun starting(description: Description) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        super.finished(description)
        Dispatchers.resetMain()
    }
}
  1. Modify your test class to use ExperimentalCoroutinesApi runTest and advanceUntilIdle()
    @OptIn(ExperimentalCoroutinesApi::class) // New addition
    internal class ConnectionsViewModelTest {

    @ExperimentalCoroutinesApi
    @get:Rule
    var mainCoroutineRule = MainCoroutineRule() // New addition
    ...
    @Test
    fun test_abcd() {
        runTest { // New addition
            ...
            val viewModel = MyViewModel()
            viewModel.foo()
            advanceUntilIdle()  // New addition
            verify { mockObject.footlooseFunction() }
        }
    }

For explanation on why to do this you can always refer to the codelab https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#3

Ne answered 16/12, 2022 at 8:14 Comment(0)
U
-1

The problem you are having stems not from runBlocking, but rather from LiveData not propagating a value without an attached observer.

I have seen many ways of dealing with this, but the simplest is to just use observeForever and a CountDownLatch.

@Test
fun testSomething() {
    runBlocking {
        viewModel.doSomething()
    }
    val latch = CountDownLatch(1)
    var result: String? = null
    viewModel.myLiveData.observeForever {
        result = it
        latch.countDown()
    }
    latch.await(2, TimeUnit.SECONDS)
    assertNotNull(result)
}

This pattern is quite common and you are likely to see many projects with some variation of it as a function/method in some test utility class/file, e.g.

@Throws(InterruptedException::class)
fun <T> LiveData<T>.getTestValue(): T? {
    var value: T? = null
    val latch = CountDownLatch(1)
    val observer = Observer<T> {
        value = it
        latch.countDown()
    }
    latch.await(2, TimeUnit.SECONDS)
    observeForever(observer)
    removeObserver(observer)
    return value
}

Which you can call like this:

val result = viewModel.myLiveData.getTestValue()

Other projects make it a part of their assertions library.

Here is a library someone wrote dedicated to LiveData testing.

You may also want to look into the Kotlin Coroutine CodeLab

Or the following projects:

https://github.com/googlesamples/android-sunflower

https://github.com/googlesamples/android-architecture-components

Unoccupied answered 23/4, 2019 at 0:24 Comment(4)
I don't think the issue is with the live data not propagating the value. If I change the executor in my viewmodel to a synchronous executor, then the test passes. So it definitely has something to do with the coroutinesTojo
Also the InstantTaskExecutorRule rule makes sure livedata posts value instantlyTojo
Did you try it? InstantTaskExecutorRule only ensure the AAC run synchronously, you still need an observer.Unoccupied
proandroiddev.com/…Unoccupied
S
-1

You don't have to change the ViewModel's code, the only change is required to properly set coroutine scope (and dispatcher) when putting ViewModel under test.

Add this to your unit test:

    @get:Rule
    open val coroutineTestRule = CoroutineTestRule()

    @Before
    fun injectTestCoroutineScope() {
        // Inject TestCoroutineScope (coroutineTestRule itself is a TestCoroutineScope)
        // to be used as ViewModel.viewModelScope fro the following reasons:
        // 1. Let test fail if coroutine launched in ViewModel.viewModelScope throws exception;
        // 2. Be able to advance time in tests with DelayController.
        viewModel.injectScope(coroutineTestRule)
    }

CoroutineTestRule.kt

    @Suppress("EXPERIMENTAL_API_USAGE")
    class CoroutineTestRule : TestRule, TestCoroutineScope by TestCoroutineScope() {

    val dispatcher = coroutineContext[ContinuationInterceptor] as TestCoroutineDispatcher

    override fun apply(
        base: Statement,
        description: Description?
    ) = object : Statement() {

        override fun evaluate() {
            Dispatchers.setMain(dispatcher)
            base.evaluate()

            cleanupTestCoroutines()
            Dispatchers.resetMain()
        }
    }
}

The code will be executed sequentially (your test code, then view model code, then launched coroutine) due to the replaced main dispatcher.

The advantages of the approach above:

  1. Write test code as normal, no need to use runBlocking or so;
  2. Whenever a crash happen in coroutine, that will fail the test (because of cleanupTestCoroutines() called after every test).
  3. You can test coroutine which uses delay internally. For that test code should be run in coroutineTestRule.runBlockingTest { } and advanceTimeBy() be used to move to the future.
Sonny answered 18/12, 2020 at 12:17 Comment(1)
No injectScope for my ViewModelNereus

© 2022 - 2024 — McMap. All rights reserved.