Home > Net >  How to Unit Test a ViewModel using Flows and LiveData with runTest?
How to Unit Test a ViewModel using Flows and LiveData with runTest?

Time:04-13

Imagine my ViewModel looks like this :

class ViewModelA(
    repository: Repository,
    ioCoroutineDispatcher: CoroutineDispatcher,
) : ViewModel() {
    val liveData : LiveData<String> = repository.getFooBarFlow().asLiveData(ioCoroutineDispatcher)
}

Imagine my Repository implementation looks like this :

class RepositoryImpl : Repository() {
    override fun getFooBarFlow() : Flow<String> = flow {
        emit("foo")
        delay(300)
        emit("bar")
    }
}

How could I unit test the fact that "foo" is emitted immediately by the LiveData, then 300ms later (no more, no less), that "bar" is emitted by the LiveData ?

You can use JUnit 4 or 5, but you have to use Kotlin 1.6, kotlinx-coroutines-test 1.6 and runTest {} instead of runBlockingTest {} (I have no issues testing with runBlockingTest {})

CodePudding user response:

The new TestCoroutineRule in coroutine-test 1.6 looks like this :

@ExperimentalCoroutinesApi
class TestCoroutineRule : TestRule {

    val testCoroutineDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testCoroutineDispatcher)

    override fun apply(base: Statement, description: Description?) = object : Statement() {
        @Throws(Throwable::class)
        override fun evaluate() {
            Dispatchers.setMain(testCoroutineDispatcher)

            base.evaluate()

            Dispatchers.resetMain()
        }
    }

    fun runTest(block: suspend TestScope.() -> Unit) = testScope.runTest { block() }
}

runTest behave a lot more differently than runBlockingTest :

runTest() will automatically skip calls to delay() and handle uncaught exceptions. Unlike runBlockingTest(), it will wait for asynchronous callbacks to handle situations where some code runs in dispatchers that are not integrated with the test module. (source)

Take note, advanceTimeBy(n) doesn't really advance the virtual coroutine time by n. The difference with the 1.5 version is it won't execute tasks scheduled at n, only tasks scheduled at n - 1. To execute tasks schedule at n, you need to use the new function runCurrent().

An example implementation for the test would be :

@ExperimentalCoroutinesApi
class DetailViewModelTest {

    @get:Rule
    val testCoroutineRule = TestCoroutineRule()

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Test
    fun getFooBarFlow() = testCoroutineRule.runTest {
        // Given
        val fakeRepository = object : Repository {
            override fun getFooBarFlow(): Flow<String> = flow {
                emit("foo")
                delay(300)
                emit("bar")
            }
        }

        // When
        val liveData = ViewModelA(fakeRepository, testCoroutineRule.testCoroutineDispatcher).liveData
        liveData.observeForever { }

        // Then
        runCurrent()
        assertEquals("foo", liveData.value)

        advanceTimeBy(300)
        runCurrent()
        assertEquals("bar", liveData.value)
    }
}

A complete implementation with some helpers can be found here : https://github.com/NinoDLC/HiltNavArgsDemo/commit/c2d84dd79c846b96d419217eb68dd7e12baedeb6

  • Related