Write unit tests for View Model — Part 1
6 min readAug 10, 2024
Introduction
This article will explore a fundamental understanding of how to write a unit test case for a ViewModel.
what is Unit Testing?
- Unit testing aims to examine each module or unit of a system to identify, analyze, and resolve defects.
- It is a crucial aspect of software development, consisting of test cases designed to verify the business logic of your code.
- Generally, testing involves ensuring that a product performs as intended. Unit testing includes test cases that check if a class functions correctly according to its specifications.
- Following are some of the testing frameworks used in Android:
- JUnit
- Mockito
- Powermock
- Robolectric
- Espresso
- Hamcrest
For a basic understanding, you can go through article for beginner level -> https://medium.com/globant/android-unit-testing-for-beginners-f64f07e07a78
Setup requires for unit testing
dependencies {
// Required -- JUnit 4 framework
testImplementation "junit:junit:$jUnitVersion"
// Optional -- Robolectric environment
testImplementation "androidx.test:core:$androidXTestVersion"
// Optional -- Mockito framework
testImplementation "org.mockito:mockito-core:$mockitoVersion"
// Optional -- mockito-kotlin
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
// Optional -- Mockk framework
testImplementation "io.mockk:mockk:$mockkVersion"
}
why do we write unit test cases?
- Validation of Functional Requirements: Unit testing ensures that the functional requirements of individual code units are met, promoting accurate functionality.
- Regression Testing: Unit tests can be reused to detect code regressions, assuring that changes or updates do not break existing functionality.
We will write unit test cases for this sample view model.
class MainViewModel(private val repository: UserRepository) : ViewModel() {
private val _users = MutableLiveData<NetworkResult<List<UserListItem>>>()
val users: LiveData<NetworkResult<List<UserListItem>>>
get() = _users
fun getUsers(){
viewModelScope.launch {
val result = repository.getUsers()
_users.postValue(result)
}
}
}
- Here we have a function called getUsers() that will invoke the UserRepository class. Once we retrieve the results, they will be assigned to LiveData.
- To test this ViewModel, we first need to mock the UserRepository class. For mocking UserRepository, we utilize MockAnnotations (import org.mockito.MockitoAnnotations).
- We can define a variable as private lateinit var UserRepository, and apply the @Mock annotation. To initialize it, we invoke MockitoAnnotations.openMocks(this) within a setup method.
The
MockitoAnnotations.openMocks()
method returns an instance ofAutoClosable
which can be used to close the resource after the test and initializes fields annotated with Mockito annotations.
- Before we start writing unit tests, it’s essential to understand annotations. JUnit is a testing framework that employs annotations to identify methods designated for testing,
- We’ve created a setUp() method marked with the @Before annotation.
- @Before runs prior to @ Test.
class MainViewModelTest {
@Mock
lateinit var repository: UserRepository
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
}
}
- We also need to define the function’s behavior in response to various outputs and test whether the correct data is being set for LiveData. For this, we will create an extension function.
- To implement this, we need to write a testDispatcher, specifically a StandardTestDispatcher, and set it up in the setup() method. Likewise, in the @After annotated method, tearDown(), we will reset it. By doing so, we will have completed the setup for our coroutines.
class MainViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@Mock
lateinit var repository: UserRepository
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
}
What is StandardTestDispatcher() do?
- In a testing environment, we always strive to ensure that our code executes on a single thread rather than multiple threads. To achieve this, we utilize TestDispatcher().
- When multiple threads are at play, our unit tests may become unreliable due to their dependence on various threads, which can affect the outcomes of these tests.
- For instance, when applying an assertion in a test case, waiting for completion renders it an unreliable scenario.
- By using StandardTestDispatcher(), we can work with multiple coroutines while ensuring that the test cases are scheduled to execute on just one thread.
- To begin testing the view model methods, we can define an
InstantTaskExecutorRule
and create our system under test (SUT) object from theMainViewModel
class.
class MainViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Mock
lateinit var repository: UserRepository
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(testDispatcher)
val sut = MainViewModel(repository)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
}
- Now we can write our test method to test the viewmodel getUser() function. Method test_GetUsers() will have annotations as @ Test.
- In this function, we need to verify whether the data we receive from the UserRepository class, specifically repository.getUsers(), is being set to LiveData.
Mockito.`when`(repository.getUsers()).thenReturn(NetworkResult.Success(emptyList()))
- Whenever the method getUsers() will get a call then return the list of users. We can assume it is returning an empty list ().To return an empty list we need to enclose it in the NetworkResult class.
- As this is a suspend function, we need to add this in the runTest{…} block.
class MainViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Mock
lateinit var repository: UserRepository
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(testDispatcher)
val sut = MainViewModel(repository)
}
@Test
fun test_GetUsers() = runTest{
Mockito.`when`(repository.getUsers()).thenReturn(NetworkResult.Success(emptyList()))
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
}
- now we can call MainViewmodel method getUsers() as sut.getUsers(). So once this method gets called as per viewmodel code coroutine will get launched it will call the repository getUsers() method “repository.getUsers()” It will provide us result from the API and get it set to LiveData.
- Now we can check if data getting set to Livedata or not. We can get the livedata value by “sut.users.getOrAwaitValue()”. And apply operation assertEquals to check if the data list we are getting is empty or not “Assert.assertEquals(0, result.data!!.size)”.
class MainViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Mock
lateinit var repository: UserRepository
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(testDispatcher)
}
@Test
fun test_GetUsers() = runTest{
Mockito.`when`(repository.getUsers()).thenReturn(NetworkResult.Success(emptyList()))
sut.getUsers()
val result = sut.users.getOrAwaitValue()
Assert.assertEquals(0, result.data!!.size)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
}
- If we execute this test case, it will fail with the error message “LiveData value was never set.” This occurs because we launch coroutines in sut.getUsers(); in our test case class, we use
testDispatcher
and arunTest{…}
block. During this execution, all coroutines will be launched but will only be scheduled on theTestScheduler
and not run. - We want to ensure that all coroutines are completed before the assert operation is executed; the data will be set in LiveData once they are finished.
- We can add “testDispatcher.scheduler.advanceUntilIdle()” to address this issue. The testDispatcher has a scheduler that executes all the coroutines. The advanceUntilIdle() function ensures that all coroutines are executed before any assertion operations are performed.
class MainViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Mock
lateinit var repository: UserRepository
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(testDispatcher)
}
@Test
fun test_GetUsers() = runTest{
Mockito.`when`(repository.getUsers()).thenReturn(NetworkResult.Success(emptyList()))
sut.getUsers()
testDispatcher.scheduler.advanceUntilIdle()
val result = sut.users.getOrAwaitValue()
Assert.assertEquals(0, result.data!!.size)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
}
- Now we will write a test case for error: the getUsers() method will return an error this time “thenReturn(NetworkResult.Error(“Something Went Wrong”))” and in assert will check the data typeof the result if it is Error type.
class MainViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Mock
lateinit var repository: UserRepository
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(testDispatcher)
}
@Test
fun test_GetUsers() = runTest{
Mockito.`when`(repository.getUsers()).thenReturn(NetworkResult.Success(emptyList()))
sut.getUsers()
testDispatcher.scheduler.advanceUntilIdle()
val result = sut.users.getOrAwaitValue()
Assert.assertEquals(0, result.data!!.size)
}
@Test
fun test_GetUsers_Error() = runTest{
Mockito.`when`(repository.getUsers()).thenReturn(NetworkResult.Error("Something Went Wrong"))
val sut = MainViewModel(repository)
sut.getUsers()
testDispatcher.scheduler.advanceUntilIdle()
val result = sut.users.getOrAwaitValue()
Assert.assertEquals(true, result is NetworkResult.Error)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
}
- In the next article, we will check how to unit test the UserRepository class.
References to read :