Reputation: 8291
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
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