Android ViewModel Coroutine Testing

Android ViewModel Coroutine Testing (Complete Guide)

Testing ViewModel coroutines is a critical skill in modern Android (MVVM).
The goal is to test business logic, StateFlow / LiveData, and coroutine behavior without Android framework dependencies.


1. Why ViewModel Coroutine Testing Is Different

ViewModels usually:

  • Use viewModelScope

  • Switch dispatchers (IO, Main)

  • Emit state via StateFlow / LiveData

❌ Problems without proper setup:

  • Tests hang

  • Dispatchers.Main crashes

  • Flaky timing issues

✅ Solution:

  • Use kotlinx-coroutines-test

  • Replace Dispatchers.Main

  • Control coroutine execution


2. Required Dependencies

testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
testImplementation("androidx.arch.core:core-testing:2.2.0")

3. Rule to Replace Dispatchers.Main (IMPORTANT)

Create a MainDispatcherRule

@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
private val dispatcher: TestDispatcher = StandardTestDispatcher()
) : TestWatcher() {

override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}

override fun finished(description: Description) {
Dispatchers.resetMain()
}
}


4. Sample ViewModel (StateFlow)

class LoginViewModel(
private val repository: LoginRepository
) : ViewModel() {

private val _uiState = MutableStateFlow(false)
val uiState: StateFlow<Boolean> = _uiState

fun login() {
viewModelScope.launch {
_uiState.value = repository.login()
}
}
}


5. Fake Repository (Best Practice)

class FakeLoginRepository : LoginRepository {
override suspend fun login(): Boolean {
delay(100)
return true
}
}

6. ViewModel Coroutine Test (StateFlow)

@OptIn(ExperimentalCoroutinesApi::class)
class LoginViewModelTest {

@get:Rule
val mainDispatcherRule = MainDispatcherRule()

@Test
fun login updates uiState to true() = runTest {
val viewModel = LoginViewModel(FakeLoginRepository())

viewModel.login()
advanceUntilIdle()

assertTrue(viewModel.uiState.value)
}
}

✅ No real delay
✅ Deterministic test
✅ No Android framework


7. Testing Loading → Success State

ViewModel

sealed class UiState {
object Loading : UiState()
object Success : UiState()
}

class MyViewModel(
private val repo: Repo
) : ViewModel() {

private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state

fun load() {
viewModelScope.launch {
delay(100)
_state.value = UiState.Success
}
}
}


Test

@Test
fun state changes from loading to success() = runTest {
val vm = MyViewModel(FakeRepo())

vm.load()
advanceUntilIdle()

assertEquals(UiState.Success, vm.state.value)
}


8. Testing SharedFlow (One-Time Events)

ViewModel

class EventViewModel : ViewModel() {

private val _events = MutableSharedFlow<String>()
val events = _events.asSharedFlow()

fun sendEvent() {
viewModelScope.launch {
_events.emit("Done")
}
}
}


Test

@Test
fun event is emitted() = runTest {
val vm = EventViewModel()
val results = mutableListOf<String>()

val job = launch {
vm.events.collect {
results.add(it)
}
}

vm.sendEvent()
advanceUntilIdle()

assertEquals(listOf("Done"), results)
job.cancel()
}


9. Testing Error Handling

class ErrorViewModel : ViewModel() {

private val _state = MutableStateFlow<String>("Idle")
val state: StateFlow<String> = _state

fun fail() {
viewModelScope.launch {
try {
throw Exception("Error")
} catch (e: Exception) {
_state.value = "Error"
}
}
}
}

Test

@Test
fun error state is set() = runTest {
val vm = ErrorViewModel()

vm.fail()
advanceUntilIdle()

assertEquals("Error", vm.state.value)
}


10. Testing withContext(Dispatchers.IO)

@Test
fun io dispatcher does not break test() = runTest {
val result = withContext(Dispatchers.IO) {
10
}
assertEquals(10, result)
}

runTest handles dispatcher switching automatically


11. Common Mistakes (Avoid These)

❌ Using runBlocking
❌ Not replacing Dispatchers.Main
❌ Using real repositories
❌ Using delays without virtual time


12. Best Practices (Must Follow)

✔ Use fake repositories
✔ Always use MainDispatcherRule
✔ Use StateFlow instead of LiveData
✔ Prefer advanceUntilIdle()
✔ Test ViewModel logic only


Final Summary

  • ViewModel coroutine testing is mandatory for stable apps

  • Replace Dispatchers.Main

  • Use runTest + fake dependencies

  • Test StateFlow & SharedFlow safely

  • No Android instrumentation needed

You may also like...