I have my viewmodel and repository like these:
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
class MyViewModel(
application: Application,
private val repository: MyRepository
) : AndroidViewModel(application) {
val myLiveData: LiveData<String> = repository.getDataFlow().filterNotNull().asLiveData()
fun loadData() {
viewModelScope.launch {
repository.fetchNewData()
}
}
}
class MyRepository {
// A Flow that emits data
private val _dataFlow = MutableStateFlow<String>("initial value")
fun getDataFlow(): Flow<String> = _dataFlow
suspend fun fetchNewData() {
for(i in 1..10) {
// Here you could fetch new data and update the _dataFlow
println("value-$i")
_dataFlow.emit("value-$i")
delay(500)
}
}
}
and the unit test is like this:
import android.app.Application
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import com.ford.digital_owners_manual.viewmodels.MyRepository
import com.ford.digital_owners_manual.viewmodels.MyViewModel
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestWatcher
import org.junit.runner.Description
@ExperimentalCoroutinesApi
class MainCoroutineRule(
val dispatcher: TestDispatcher = StandardTestDispatcher()
) : TestWatcher() {
val testScope = TestScope(dispatcher)
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain() // Reset Dispatchers.Main to the original Main dispatcher
}
}
@ExperimentalCoroutinesApi
class MyViewModelTest {
@get:Rule
val rule = InstantTaskExecutorRule()
@get:Rule
val coroutineRule = MainCoroutineRule()
private lateinit var viewModel: MyViewModel
private lateinit var repository: MyRepository
@Before
fun setUp() {
repository = MyRepository()
viewModel = MyViewModel(
application = mockk<Application>(),
repository
)
}
@Test
fun `loadData should update live data value`() = coroutineRule.testScope.runTest {
val observer: Observer<String> = mockk(relaxed = true)
// When
viewModel.myLiveData.observeForever(observer)
viewModel.loadData()
runBlocking {
launch {
repository.fetchNewData()
}
//coVerify { observer.onChanged(any())}
}
// Then
assertEquals("Sample Data", viewModel.myLiveData.getOrAwaitValue())
coVerify { observer.onChanged(any())}
//verify { repository.getDataFlow() }
}
}
getOrAwaitValue is defined here, copied from other SO answers.
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
[email protected](this)
}
}
this.observeForever(observer)
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
coVerify says observer.onChanged(any()) never called.
assertEquals complains LiveData value was never set.