How to unit test coroutine when it contains corout

2019-08-04 17:50发布

问题:

When I add a coroutine delay() in my view model, the remaining part of the code will not be executed.

This is my demo code:

class SimpleViewModel : ViewModel(), CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Unconfined

    var data = 0

    fun doSomething() {
        launch {
            delay(1000)
            data = 1
        }
    }
}

class ScopedViewModelTest {

    @Test
    fun coroutineDelay() {
        // Arrange
        val viewModel = SimpleViewModel()

        // ActTes
        viewModel.doSomething()

        // Assert
        Assert.assertEquals(1, viewModel.data)
    }
}

I got the assertion result:

java.lang.AssertionError: 
Expected :1
Actual   :0

Any idea how to fix this?

回答1:

You start a coroutine which suspends for 1 second before setting data to 1. Your test just invokes doSomething but does not wait until data is actually being set. If you add another, longer delay, to the test it will, work:

@Test     
fun coroutineDelay() = runBlocking {
    ...
    viewModel.doSomething()
    delay(1100)
    ...
}

You can also make the coroutine return a Deferred which you can wait on:

fun doSomething(): Deferred<Unit> {
    return async {
        delay(1000)
        data = 1
    }
}

With await there's no need to delay your code anymore:

val model = SimpleViewModel()
model.doSomething().await()


回答2:

The first issue in your code is that SimpleViewModel.coroutineContext has no Job associated with it. The whole point of making your view model a CoroutineScope is the ability to centralize the cancelling of all coroutines it starts. So add the job as follows (note the absence of a custom getter):

class SimpleViewModel : ViewModel(), CoroutineScope {

    override val coroutineContext = Job() + Dispatchers.Unconfined

    var data = 0

    fun doSomething() {
        launch {
            delay(1000)
            data = 1
        }
    }
}

Now your test code can ensure it proceeds to the assertions only after all the jobs your view model launched are done:

class ScopedViewModelTest {

    @Test
    fun coroutineDelay() {
        // Arrange
        val viewModel = SimpleViewModel()

        // ActTes
        viewModel.doSomething()

        // Assert
        runBlocking {
            viewModel.coroutineContext[Job]!!.children.forEach { it.join() }
        }
        Assert.assertEquals(1, viewModel.data)
    }
}