MVVM Unit Testing Poster

MVVM Unit testing on Android

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

before getting into the MVVM testing, I strongly recommend you check the examples on MVVM.

MVVM with Kotlin Coroutines and Retrofit [Example] – Howtodoandroid

MVVM With Retrofit and Recyclerview in Kotlin [Example] (howtodoandroid.com)

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'
Code language: JavaScript (javascript)

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 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 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 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 } }
Code language: JavaScript (javascript)

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)) } }
Code language: PHP (php)

In the first test case, I 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. 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'
Code language: JavaScript (javascript)

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) }
Code language: JavaScript (javascript)

In the @Before, we 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"))))
Code language: JavaScript (javascript)

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 }
Code language: JavaScript (javascript)

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

Finally, we can use assertEquals the 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) } } }
Code language: JavaScript (javascript)

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'
Code language: JavaScript (javascript)

Writing tests for Repository

Testing the repository is very simple compared with the ViewModel. here is my main repository 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) }
Code language: JavaScript (javascript)

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>()))
Code language: CSS (css)

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()) } } }
Code language: JavaScript (javascript)

Writing tests for ApiService

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

testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
Code language: JavaScript (javascript)

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) }
Code language: JavaScript (javascript)

In 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() } }
Code language: JavaScript (javascript)

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


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *