Ricardo
Ricardo

Reputation: 8291

Android: unit testing of LiveData and Flow

I'm trying to write unit testing for my ViewModel but I don't know how to deal with LiveData functions.

Specifically I'm not able to validate all the values that receive the LiveData Observer.

Regarding I have a Flow Use case that emit values and then is pased as a LiveData, what is the best approach to test operation function?

In the code below you can find that I'm only able to read the value "endLoading", but I want to check all the values: "startLoading", "Hello Dummy $input", "endLoading"

MainViewModel.kt

class MainViewModel(val useCase: DummyUseCase = DummyUseCase()): ViewModel() {
    fun operation(value: Int): LiveData<String> = useCase.invoke(value)
        .transform { response ->
            emit(response)
        }.onStart {
            emit("startLoading")
        }.catch {
            emit("ERROR")
        }.onCompletion {
            emit("endLoading")
        }.asLiveData(viewModelScope.coroutineContext)
}

MainViewModelTest.kt

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test

@ExperimentalCoroutinesApi
class MainViewModelTest {
    //region Setup
    @get:Rule
    val rule = InstantTaskExecutorRule()
    private val testDispatcher = TestCoroutineDispatcher()

    @MockK private lateinit var stateObserver: Observer<String>
    @MockK private lateinit var useCase: DummyUseCase
    private lateinit var viewModel: MainViewModel

    @Before
    fun setup() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        Dispatchers.setMain(testDispatcher)
        viewModel = MainViewModel(useCase)
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
    //endregion

    @Test // AAA testing
    fun `when my flow succeeds, return a state String`() {
        runBlocking {
            //Arrange
            val input = 10
            coEvery { useCase.invoke(input) }.returns(flow {
                emit("Hello Dummy $input")
            })

            //Act
            val actual = viewModel.operation(input).apply {
                observeForever(stateObserver)
            }

            //Assert
            // I want to assert here every value received in the observer of the "actual" LiveData
            // How? :(
            assertNotNull(actual.value) // is always "endLoading"
        }
    }
}

Upvotes: 2

Views: 2598

Answers (1)

Bennik2000
Bennik2000

Reputation: 1152

You can test the LiveData using a custom Observer<T> implementation. Create an observer which records all emmited values and lets you assert against the history.

The Observer which records the values may look like this:

class TestableObserver<T> : Observer<T> {
    private val history: MutableList<T> = mutableListOf()

    override fun onChanged(value: T) {
        history.add(value)
    }

    fun assertAllEmitted(values: List<T>) {
        assertEquals(values.count(), history.count())

        history.forEachIndexed { index, t ->
            assertEquals(values[index], t)
        }
    }
}

You can assert if all given values were emitted by the LiveData using the assertAllEmitted(...) function.

The test function will use an instance of the TestableObserver class instead of a mocked one:

@Test // AAA testing
fun `when my flow succeeds, return a state String`() {
    runBlocking {
        //Arrange
        val stateObserver = TestableObserver<String>()

        val input = 10
        coEvery { useCase.invoke(input) }.returns(flow {
            emit("Hello Dummy $input")
        })

        //Act
        val actual = viewModel.operation(input).apply {
            observeForever(stateObserver)
        }

        //Assert
        stateObserver.assertAllEmitted(
            listOf(
                "startLoading",
                "Hello Dummy 10",
                "endLoading"
            )
        )
    }
}

Asserting the history of LiveData may be possible using mocking frameworks and assertion frameworks however, I think implementing this testable observer is more readable.

Upvotes: 2

Related Questions