Unit Testing ViewModel with Kotlin Coroutines and LiveData
- Authors
- Name
- Amit Shekhar
- Published on
To unit test a ViewModel with Kotlin Coroutines and LiveData, we mock the data layer using Mockito, use a TestCoroutineRule to control the coroutines, and then verify the values that the LiveData emits.
In this blog, we are going to learn how to write the unit test for ViewModel with Kotlin Coroutines and LiveData that follows a basic MVVM Architecture. We will write the unit-test for the ViewModel which makes a network call and then, validate if our ViewModel is working as expected or not.
I am Amit Shekhar, Founder @ Outcome School, I have taught and mentored many developers, and their efforts landed them high-paying tech jobs, helped many tech companies in solving their unique problems, and created many open-source libraries being used by top companies. I am passionate about sharing knowledge through open-source, blogs, and videos.
I teach AI and Machine Learning, and Android at Outcome School.
Join Outcome School and get a high-paying tech job:
Let's get started.
I will be using this project for the implementation part. If you have not gone through the project, you should go through it and then come back. The project follows a basic MVVM Architecture for simplicity. You can find the complete code for unit testing mentioned in the blog in the project itself.
We will take the example of SingleNetworkCallViewModel which is present in the project.
Basically, this SingleNetworkCallViewModel is a ViewModel that is associated with SingleNetworkCallActivity which triggers the ViewModel to fetch the list of users to render into the UI. The SingleNetworkCallViewModel, then asks the data layer for the list of users using the ApiHelper. As you can see below, the ViewModel uses Kotlin Coroutines and LiveData.
class SingleNetworkCallViewModel(
private val apiHelper: ApiHelper,
private val dbHelper: DatabaseHelper
) : ViewModel() {
private val uiState = MutableLiveData<UiState<List<ApiUser>>>()
init {
fetchUsers()
}
private fun fetchUsers() {
viewModelScope.launch {
uiState.postValue(UiState.Loading)
try {
val usersFromApi = apiHelper.getUsers()
uiState.postValue(UiState.Success(usersFromApi))
} catch (e: Exception) {
uiState.postValue(UiState.Error(e.toString()))
}
}
}
fun getUiState(): LiveData<UiState<List<ApiUser>>> {
return uiState
}
}
Now, we have to write the unit-test for this ViewModel which uses Kotlin Coroutines and LiveData.
First, we need to set up our dependencies for the test like below:
testImplementation 'junit:junit:4.12'
testImplementation "org.mockito:mockito-core:3.3.3"
testImplementation 'androidx.arch.core:core-testing:2.1.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.4'
Make sure to use the latest version which is applicable when you are reading this article. This is important as there are many bug fixes coming along with every release.
Let's move to the test package where we will be writing the unit-test for the ViewModel.
Now, we need to create the TestRule, we will name it as TestCoroutineRule and put that inside the utils package.
@ExperimentalCoroutinesApi
class TestCoroutineRule : TestRule {
private val testCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
testCoroutineScope.cleanupTestCoroutines()
}
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest { block() }
}
A quick note for you
No matter which tech domain you work in, get familiar with these topics:
- LLM
- RAG
- MCP
- Agent
- Fine-tuning
- Quantization
We put it all together in one video:
AI Engineering Explained: LLM, RAG, MCP, Agent, Fine-Tuning, and Quantization
No need to stop reading - bookmark it and watch later when you get time. Future you will thank you.
Now, let's get back to the topic.
Why the above TestRule?
- During the unit-test, it enables the main dispatcher to use TestCoroutineDispatcher.
- After the test, it resets and cleanup.
Now, we will create SingleNetworkCallViewModelTest at the appropriate place inside the test package like below:
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class SingleNetworkCallViewModelTest {
@get:Rule
val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule()
@get:Rule
val testCoroutineRule = TestCoroutineRule()
@Mock
private lateinit var apiHelper: ApiHelper
@Mock
private lateinit var databaseHelper: DatabaseHelper
@Mock
private lateinit var uiStateObserver: Observer<UiState<List<ApiUser>>>
@Before
fun setUp() {
// do something if required
}
@Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
testCoroutineRule.runBlockingTest {
doReturn(emptyList<ApiUser>())
.`when`(apiHelper)
.getUsers()
val viewModel = SingleNetworkCallViewModel(apiHelper, databaseHelper)
viewModel.getUiState().observeForever(uiStateObserver)
verify(apiHelper).getUsers()
verify(uiStateObserver).onChanged(UiState.Success(emptyList()))
viewModel.getUiState().removeObserver(uiStateObserver)
}
}
@Test
fun givenServerResponseError_whenFetch_shouldReturnError() {
testCoroutineRule.runBlockingTest {
val errorMessage = "Error Message For You"
doThrow(RuntimeException(errorMessage))
.`when`(apiHelper)
.getUsers()
val viewModel = SingleNetworkCallViewModel(apiHelper, databaseHelper)
viewModel.getUiState().observeForever(uiStateObserver)
verify(apiHelper).getUsers()
verify(uiStateObserver).onChanged(
UiState.Error(RuntimeException(errorMessage).toString())
)
viewModel.getUiState().removeObserver(uiStateObserver)
}
}
@After
fun tearDown() {
// do something if required
}
}
Here we have used the InstantTaskExecutorRule which is needed to test code with LiveData. If we do not use this, we will get the RuntimeException related to Looper in Android.
Here, We have mocked ApiHelper, DatabaseHelper, and etc and written two tests:
- When the server gives 200, it should return success to the UI layer.
- When the server gives an error, it should return an error to the UI layer.
In the first one, we have mocked the ApiHelper to return the success with an empty list. Then, we fetch and verify.
Similarly in the second one, we have mocked the ApiHelper to return the error. Then, we fetch and verify.
This way, we are able to write the unit test for ViewModel with Kotlin Coroutines and LiveData that follows a basic MVVM Architecture.
To master Unit Testing, ViewModel, LiveData, Coroutines Internals, and MVVM Architecture hands-on, we have a complete Android Program by Outcome School that covers these topics in depth along with Flow API and Jetpack Components.
You can run and test the end-to-end implementation in this project.
Stay updated: Subscribe to our newsletter to get our latest AI and Machine Learning blogs straight to your inbox.
Frequently Asked Questions
Why do we need InstantTaskExecutorRule when testing a ViewModel with LiveData?
We need InstantTaskExecutorRule because it is required to test code that uses LiveData. If we do not use this rule, we will get a RuntimeException related to Looper in Android. So, we add it as a rule in the ViewModel test class along with our coroutine test rule.
Why do we need a custom TestCoroutineRule for coroutine tests?
We need it so that the main dispatcher uses TestCoroutineDispatcher during the unit test. After the test, the rule resets the main dispatcher and cleans up the test coroutines. It also gives us a runBlockingTest function to run our test code inside the test coroutine scope.
How do we test the error case of a ViewModel that uses Coroutines?
We mock the ApiHelper to throw an exception when getUsers is called. Then, we create the ViewModel, observe its UI state, verify that getUsers was called, and verify that the observer received an error state with the matching exception message. Finally, we remove the observer.
Should we use the same test dependency versions as the example?
No. We should use the latest versions that are applicable when we are reading. This is important because many bug fixes come along with every release. The example uses JUnit, Mockito, the AndroidX core testing library, and the Kotlin coroutines test library as test dependencies.
That's it for now.
Thanks
Amit Shekhar
Founder @ Outcome School
You can connect with me on:
Follow Outcome School on:
