In this tutorial, I am explaining about writing test cases for MVVM on android. Unit testing is testing every unit of your code. Unit testing is a must to build robust android applications. It is an important element while building quality applications. Unit tests are the smallest (individually) and with the least execution time.

Benefits of Unit Testing

  • Helps in finding bugs early.
  • As it helps to find the bugs in the early stage of development and reduces the development cost and time.
  • Simplifies refactoring and provides documentation.

Following are some of the testing frameworks used in Android:

  • JUnit
  • Mockito
  • Powermock
  • Robolectric
  • Espresso
  • Hamcrest

Setup Unit Testing Dependencies

In your Android Studio Project, the following are the three important packages inside the src folder:

app/src/main/java/ —  Main java source code folder.
app/src/test/java/ —  Local unit test folder.
app/src/androidTest/java/ — Instrumentation test folder.

Test Folder

test/java/  folder is where the JUnit4 test cases will be written. Local Unit Testing cannot have Android APIs. The test folder classes are compiled and run on the JVM only.

In this example, we are going to use JUnit and Mockito framework to write the Unit Test.

Unit Testing Dependencies

Whenever you start a new Android Studio Project, JUnit dependency is already present in the build.gradle(also Expresso Dependency).

testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
testImplementation 'org.mockito:mockito-core:2.28.2'
androidTestImplementation 'org.mockito:mockito-android:2.24.5'

You can see here we have testImplementation and androidTestImplementationtestImplementation are the libraries available inside the test package. And the same way if you want the library to be available inside androidTest package you need to use androidTestImplementation.

  • Junit: It is a “Unit Testing” framework for Java Applications. It is an automation framework for Unit as well as UI Testing. It contains annotations such as @Test, @Before, @After etc.
  • Mockito: Mockito mocks (or fakes) the dependencies required by the class being tested. It provides annotations such as @Mock.

Before writing test cases we need to understand the JUnit annotations.

JUnit Annotations

@Test – This annotation is a replacement of org.junit.TestCase which indicates that the public void method to which it is attached can be executed as a Test Case.

@Before – This annotation is used if you want to execute some statements such as preconditions before each test case.

@BeforeClass – This annotation is used if you want to execute some statements before all the test cases for e.g. test connection must be executed before all the test cases.

@After – This annotation can be used if you want to execute some statements after each Test case for e.g resetting variables, deleting temporary files, variables, etc.

@AfterClass – This annotation can be used if you want to execute some statements after all test cases for e.g. Releasing resources after executing all test cases.

How to write Simple Unit Test

To get started with JUnit testing, I have created a simple function to validate the movie.

object ValidationUtil {

    fun validateMovie(movie: Movie) : Boolean {
        if (movie.name.isNotEmpty() && movie.category.isNotEmpty()) {
            return true
        }
        return false
    }
}

The validateMovie() function check for the name and category movie of the movie is not empty. If it’s not empty it will return true, if not then return false.

Write Unit Test

Create the ValidationUtilTest class and write the unit test case for the validateMovie() function.

@RunWith(JUnit4::class)
class ValidationUtilTest {

    @Test
    fun validateMovieTest() {
        val movie = Movie("test","testUrl","main")
        assertEquals(true, ValidationUtil.validateMovie(movie))
    }

    @Test
    fun validateMovieEmptyTest() {
        val movie = Movie("","testUrl","main")
        assertEquals(false, ValidationUtil.validateMovie(movie))
    }

}

In the first test case, I have created a Movie object with a name and category, both are empty. So this validate function should return true. To check that, used Junit assertEquals function to check the result and expected result. Like that, In the second test case, the title is empty. to the validateMovie function should return false.

below is the result of the test cases.

Junit test result

Writing tests for ViewModel

I have created a simple ViewModel class, that can get a list of movies from API using the repository.

class MainViewModel constructor(private val mainRepository: MainRepository) : ViewModel() {

    val errorMessage = MutableLiveData<String>()
    val movieList = MutableLiveData<List<Movie>>()
    var job: Job? = null
    private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        onError("Exception handled: ${throwable.localizedMessage}")
    }
    val loading = MutableLiveData<Boolean>()

    fun getAllMovies() {

        job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
            loading.postValue(true)
            val response = mainRepository.getAllMovies()
            withContext(Dispatchers.Main) {
                if (response.isSuccessful) {
                    movieList.postValue(response.body())
                    loading.value = false
                } else {
                    onError("Error : ${response.message()} ")
                }
            }
        }

    }

    private fun onError(message: String) {
        errorMessage.value = message
        loading.value = false
    }

    override fun onCleared() {
        super.onCleared()
        job?.cancel()
    }

}

let’s create the test for the ViewModel.

The MainViewModel takes the MainRespository as a parameter. So we need to mock the repository class. To mock, we have the Mockito library. add the following dependencies in the module build Gradle file.

testImplementation 'org.mockito:mockito-core:2.28.2'
androidTestImplementation 'org.mockito:mockito-android:2.24.5'

Also, the ViewModel class uses coroutines to execute the retrofit API calls. So, we need to annotate the test class with @ExperimentalCoroutinesApi annotation.

Also, we need to annotate our class LoginViewModelTest with @RunWith(JUnit4::class). With this JUnit will invoke the class it references to run the tests in that class instead of the runner built into JUnit. We can define any custom runner depending on our requirements.

To work with the coroutine suspend function we need to use TestCoroutineDispatcher.

@ExperimentalCoroutinesApi
@RunWith(JUnit4::class)
class MainViewModelTest {


    private val testDispatcher = TestCoroutineDispatcher()
    lateinit var mainViewModel: MainViewModel
    lateinit var mainRepository: MainRepository

    @Mock
    lateinit var apiService: RetrofitService

    @get:Rule
    val instantTaskExecutionRule: InstantTaskExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        Dispatchers.setMain(testDispatcher)
        mainRepository = MainRepository(apiService)
        mainViewModel = MainViewModel(mainRepository)
    }

In the @Before, we have configured the mock and dispatchers. Next, will see the main test case.

The usage of Mockito can also be used based on conditions using when() and thenReturn(). A simple example of this could be as below,

Mockito.`when`(mainRepository.getAllMovies())
                .thenReturn(Response.success(listOf<Movie>(Movie("movie", "", "new"))))

The above expression returns a List of Movies when the main repository method getAllMovies is called providing the inputs.

We are done with the main repository mocking. next, we need to call the ViewModel function and verify the expected result.

So, the result of the getAllMovies() function will be stored in MutableLiveData(), to get the value from the Live data we need to use the below extension function.

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)
    try {
        afterObserve.invoke()
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }
    } finally {
        this.removeObserver(observer)
    }
    @Suppress("UNCHECKED_CAST")
    return data as T
}

The getOrAwaitValue extension function wait unit the value observed and return the value.

Finally, we can use assertEquals function to verify the test case.

@ExperimentalCoroutinesApi
@RunWith(JUnit4::class)
class MainViewModelTest {


    private val testDispatcher = TestCoroutineDispatcher()
    lateinit var mainViewModel: MainViewModel
    lateinit var mainRepository: MainRepository

    @Mock
    lateinit var apiService: RetrofitService

    @get:Rule
    val instantTaskExecutionRule: InstantTaskExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        Dispatchers.setMain(testDispatcher)
        mainRepository = MainRepository(apiService)
        mainViewModel = MainViewModel(mainRepository)
    }

    @Test
    fun getAllMoviesTest() {
        runBlocking {
            Mockito.`when`(mainRepository.getAllMovies())
                .thenReturn(Response.success(listOf<Movie>(Movie("movie", "", "new"))))
            mainViewModel.getAllMovies()
            val result = mainViewModel.movieList.getOrAwaitValue()
            assertEquals(listOf<Movie>(Movie("movie", "", "new")), result)
        }
    }


    @Test
    fun `empty movie list test`() {
        runBlocking {
            Mockito.`when`(mainRepository.getAllMovies())
                .thenReturn(Response.success(listOf<Movie>()))
            mainViewModel.getAllMovies()
            val result = mainViewModel.movieList.getOrAwaitValue()
            assertEquals(listOf<Movie>(), result)
        }
    }
    
}

As we are using Kotlin coroutines we might need to add test dependencies related to them.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0'

Writing tests for Repository

Testing the repository is very simple compared with the ViewModel. here is my mainRepository implementation class.

class MainRepository constructor(private val retrofitService: RetrofitService) {

    suspend fun getAllMovies() = retrofitService.getAllMovies()

}

Repository class having RetrofitService as a constructor parameter. So we need to create the mock for the RetrofitService in the @before function. Also, to work with mockito annotation we need to initialize the mock in the @before function.

lateinit var mainRepository: MainRepository

    @Mock
    lateinit var apiService: RetrofitService

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        mainRepository = MainRepository(apiService)
    }

Next, write a test case to test the get all movies functions. First, mock the getAllMovies() function with Mockito.

Mockito.`when`(apiService.getAllMovies()).thenReturn(Response.success(listOf<Movie>()))

Then, call the repository function and verify the test case using the assertEquals function.

@RunWith(JUnit4::class)
class MainRepositoryTest {

    lateinit var mainRepository: MainRepository

    @Mock
    lateinit var apiService: RetrofitService

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        mainRepository = MainRepository(apiService)
    }

    @Test
    fun `get all movie test`() {
        runBlocking {
            Mockito.`when`(apiService.getAllMovies()).thenReturn(Response.success(listOf<Movie>()))
            val response = mainRepository.getAllMovies()
            assertEquals(listOf<Movie>(), response.body())
        }

    }

}

Writing tests for ApiService

To test the APIService we need mock web server dependency to be added to the module-level Gradle file:

testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'

We need to create the MockWebServer instance and use it with Retrofit to get the expected results. Here we set the response data to the webserver to be returned on method calls. So, configure the mockWebserver on the @before function.

lateinit var mockWebServer: MockWebServer

    lateinit var apiService: RetrofitService
    lateinit var gson: Gson

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        gson = Gson()
        mockWebServer = MockWebServer()
        apiService = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build().create(RetrofitService::class.java)
    }

On the test case mock the API call result into mockWebServer response and call the API call method. And then verify the request-response and expected response using the assertEqual function.

class RetrofitServiceTest {
    
    lateinit var mockWebServer: MockWebServer
    lateinit var apiService: RetrofitService
    lateinit var gson: Gson

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        gson = Gson()
        mockWebServer = MockWebServer()
        apiService = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build().create(RetrofitService::class.java)
    }


    @Test
    fun `get all movie api test`() {
        runBlocking {
             val mockResponse = MockResponse()
            mockWebServer.enqueue(mockResponse.setBody("[]"))
            val response = apiService.getAllMovies()
            val request = mockWebServer.takeRequest()
            assertEquals("/movielist.json",request.path)
            assertEquals(true, response.body()?.isEmpty() == true)
        }
    }

    @After
    fun teardown() {
        mockWebServer.shutdown()
    }
    
}

That’s it. Hope this will be helpful for you. Thanks for reading. You can download this example on GITHUB.

Leave a Reply

Your email address will not be published.