Unit Testing ViewModel with Kotlin Flow and StateFlow
- Authors
- Name
- Amit Shekhar
- Published on
I am Amit Shekhar, Co-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.
Join Outcome School and get high paying tech job: Outcome School
Before we start, I would like to mention that, I have released a video playlist to help you crack the Android Interview: Check out Android Interview Questions and Answers.
In this blog, we are going to learn how to write the unit test for ViewModel with Kotlin Flow and StateFlow 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.
This blog is a part of the series I have written on Flow API in Kotlin:
- Mastering Flow API in Kotlin
- Creating Flow Using Flow Builder in Kotlin
- Terminal Operators in Kotlin Flow
- Cold Flow vs Hot Flow
- StateFlow and SharedFlow
- Long-running tasks in parallel with Kotlin Flow
- Retry Operator in Kotlin Flow
- Retrofit with Kotlin Flow
- Room Database with Kotlin Flow
- Kotlin Flow Zip Operator for Parallel Multiple Network Calls
- Instant Search Using Kotlin Flow Operators
- Exception Handling in Kotlin Flow
- callbackFlow - Callback to Flow API in Kotlin
- Unit Testing ViewModel with Kotlin Flow and StateFlow - YOU ARE HERE
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 Flow and StateFlow.
class SingleNetworkCallViewModel(
private val apiHelper: ApiHelper,
private val dbHelper: DatabaseHelper,
private val dispatcherProvider: DispatcherProvider
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<ApiUser>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<ApiUser>>> = _uiState
init {
fetchUsers()
}
private fun fetchUsers() {
viewModelScope.launch(dispatcherProvider.main) {
_uiState.value = UiState.Loading
apiHelper.getUsers()
.flowOn(dispatcherProvider.io)
.catch { e ->
_uiState.value = UiState.Error(e.toString())
}
.collect {
_uiState.value = UiState.Success(it)
}
}
}
}
Now, we have to write the unit-test for this ViewModel which uses Kotlin Flow and StateFlow.
First, we need to set up our dependencies for the test like below:
testImplementation 'junit:junit:4.13.2'
testImplementation "org.mockito:mockito-core:3.4.6"
testImplementation 'androidx.arch.core:core-testing:2.1.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
testImplementation 'app.cash.turbine:turbine:0.11.0'
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.
Note: Here, we are using Turbine
- A small testing library for Kotlin Flow.
First, we need to have a way to change the dispatchers during the unit testing in our project.
For this, we will create an interface DispatcherProvider
and the implementation of the interface as a DefaultDispatcherProvider
and put that inside the utils package as below:
interface DispatcherProvider {
val main: CoroutineDispatcher
val io: CoroutineDispatcher
val default: CoroutineDispatcher
}
class DefaultDispatcherProvider : DispatcherProvider {
override val main: CoroutineDispatcher
get() = Dispatchers.Main
override val io: CoroutineDispatcher
get() = Dispatchers.IO
override val default: CoroutineDispatcher
get() = Dispatchers.Default
}
This DefaultDispatcherProvider
will be used inside the ViewModel. Check the project to understand more about it.
Let's move to the test package where we will be writing the unit-test for the ViewModel.
Now, we need to create the TestDispatcherProvider
class by implementing DispatcherProvider
interface and put that inside the utils package.
@ExperimentalCoroutinesApi
class TestDispatcherProvider : DispatcherProvider {
private val testDispatcher = UnconfinedTestDispatcher()
override val main: CoroutineDispatcher
get() = testDispatcher
override val io: CoroutineDispatcher
get() = testDispatcher
override val default: CoroutineDispatcher
get() = testDispatcher
}
Why the above TestDispatcherProvider?
During the unit-test, it helps us in using UnconfinedTestDispatcher
wherever required. Check the project to understand more about it.
Now, we will create SingleNetworkCallViewModelTest
at the appropriate place inside the test package like below:
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class SingleNetworkCallViewModelTest {
@Mock
private lateinit var apiHelper: ApiHelper
@Mock
private lateinit var databaseHelper: DatabaseHelper
private lateinit var dispatcherProvider: DispatcherProvider
@Before
fun setUp() {
dispatcherProvider = TestDispatcherProvider()
}
@Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
runTest {
doReturn(flowOf(emptyList<ApiUser>())).`when`(apiHelper).getUsers()
val viewModel = SingleNetworkCallViewModel(apiHelper, databaseHelper, dispatcherProvider)
viewModel.uiState.test {
assertEquals(UiState.Success(emptyList<List<ApiUser>>()), awaitItem())
cancelAndIgnoreRemainingEvents()
}
verify(apiHelper).getUsers()
}
}
@Test
fun givenServerResponseError_whenFetch_shouldReturnError() {
runTest {
val errorMessage = "Error Message For You"
doReturn(flow<List<ApiUser>> {
throw IllegalStateException(errorMessage)
}).`when`(apiHelper).getUsers()
val viewModel = SingleNetworkCallViewModel(apiHelper, databaseHelper, dispatcherProvider)
viewModel.uiState.test {
assertEquals(
UiState.Error(IllegalStateException(errorMessage).toString()),
awaitItem()
)
cancelAndIgnoreRemainingEvents()
}
verify(apiHelper).getUsers()
}
}
@After
fun tearDown() {
// do something if required
}
}
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 Flow and StateFlow that follows a basic MVVM Architecture.
You can run and test the end-to-end implementation in this project.
Master Kotlin Flow from here: Kotlin Flow API
Prepare yourself for Android Interview: Android Interview Questions
That's it for now.
Thanks
Amit Shekhar
Co-Founder @ Outcome School
You can connect with me on:
Follow Outcome School on: