Kotlin RxJava Retrofit MVVM Unit Test Cases
This article is about writing unit test cases for a viewmodel implemented using MVVM design pattern with Retrofit and Mockito Unit Test cases
RetroViewModelFactory:
This class injects retrofit instance and sends API service instance as argument to ViewModel (RetroRXViewModel)
RetroViewModelFactory:
class RetroCoroutineViewModelFactory : ViewModelProvider.Factory {
@Inject
lateinit var retrofit: Retrofit
lateinit var apiService: APIService
@ExperimentalStdlibApi
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
DaggerRetroRxComponent.create().inject(this)
apiService = retrofit.create(APIService::class.java)
if (modelClass.isAssignableFrom(RetroCoroutineViewModel::class.java)) {
return RetroCoroutineViewModel(Dispatchers.Main,apiService) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
RetroViewModel:
This ViewModel makes API call and fetches data from it.
Used RXJava to do API call on Scheduler.io and fetch results on AndroidSchedulers.mainThread()
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun fetchRetroInfo(){
compositeDisposable.add(apiService.makeRequest()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableSingleObserver<List<RetroRxModel>>(){
override fun onSuccess(t: List<RetroRxModel>) {
mutableLiveData.value = t
loading.value = false
}
override fun onError(e: Throwable) {
e.printStackTrace()
postLoadError.value = e.message
loading.value = false
// onError(e.localizedMessage)
}
})
)
}
APIService Interface:
This interface has methods to interact with API service
fun makeRequest() — -> Fetches data from API
interface APIService {
// https://jsonplaceholder.typicode.com/posts
@GET("/posts")
fun makeRequest(): Single<List<RetroRxModel>>
@GET("/posts")
suspend fun fetchPosts(): Response<List<RetroRxModel>>
@GET("/posts")
suspend fun fetchUserPosts(): Response<List<RetroRxModel>>
}
Mockito Unit Test case to Test the ViewModel(RetroRXViewModelToTest):
This class unit tests viewmodel
The below variables to be declared:
// A JUnit Test Rule that swaps the background executor used by
// the Architecture Components with a different one which executes each task synchronously.
// You can use this rule for your host side tests that use Architecture Components.
@Rule
@JvmField
var rule = InstantTaskExecutorRule()
APIService to be mocked to bypass API call.
@Mock
lateinit var apiService: APIService
Create Dummy Data to be returned when API call is made with mocked data
@Test
fun fetchRetroInfoTest_success(){
var retroRxModel = RetroRxModel("tile","body","1")
var retroRXModelList = ArrayList<RetroRxModel>()
retroRXModelList.add(retroRxModel)
single = Single.just(retroRXModelList)
if(apiService!=null){
Mockito.`when`(apiService.makeRequest()).thenReturn(single)
}
retroRXViewModel.fetchRetroInfo()
Assert.assertEquals(1,retroRXViewModel.mutableLiveData.value?.size)
Assert.assertEquals(loading,retroRXViewModel.loading.value)
}
As RXJava is used ,in order to return immediately during unittest when making API call on ioThread , we need create a new rule to return immediately during unittest to make Rxjava run synchronously .
Create a new rule for RXJava to run synchronously
class RxImmediateSchedulerRule : TestRule {
private val immediate = object : Scheduler() {
override fun createWorker(): Worker {
return ExecutorScheduler.ExecutorWorker(Executor { it.run() },false)
}
override fun scheduleDirect(run: Runnable, delay: Long, unit: TimeUnit): Disposable {
// this prevents StackOverflowErrors when scheduling with a delay
return super.scheduleDirect(run, 0, unit)
}
}
override fun apply(base: Statement?, description: Description?): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
RxJavaPlugins.setInitIoSchedulerHandler { scheduler -> immediate }
RxJavaPlugins.setInitComputationSchedulerHandler { scheduler -> immediate }
RxJavaPlugins.setInitNewThreadSchedulerHandler { scheduler -> immediate }
RxJavaPlugins.setInitSingleSchedulerHandler { scheduler -> immediate }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler -> immediate }
try {
base?.evaluate()
} finally {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
}
}
}
Complete TestCase Code:
@RunWith(MockitoJUnitRunner::class)
class RetroRXViewModelToTest {
// A JUnit Test Rule that swaps the background executor used by
// the Architecture Components with a different one which executes each task synchronously.
// You can use this rule for your host side tests that use Architecture Components.
@Rule
@JvmField
var rule = InstantTaskExecutorRule()
// Test rule for making the RxJava to run synchronously in unit test
companion object {
@ClassRule
@JvmField
val schedulers = RxImmediateSchedulerRule()
}
@Mock
lateinit var apiService: APIService
lateinit var retroRXViewModel: RetroRXViewModel
private lateinit var single: Single<List<RetroRxModel>>
private var loading: Boolean = false
@Before
fun setUp() {
// initialize the ViewModed with a mocked github api
retroRXViewModel = RetroRXViewModel(apiService)
}
@Test
fun fetchRetroInfoTest_success(){
var retroRxModel = RetroRxModel("tile","body","1")
var retroRXModelList = ArrayList<RetroRxModel>()
retroRXModelList.add(retroRxModel)
single = Single.just(retroRXModelList)
if(apiService!=null){
Mockito.`when`(apiService.makeRequest()).thenReturn(single)
}
retroRXViewModel.fetchRetroInfo()
Assert.assertEquals(1,retroRXViewModel.mutableLiveData.value?.size)
Assert.assertEquals(loading,retroRXViewModel.loading.value)
}
@Test
fun fetchRetroInfoTest_Failure_Scenario(){
single = Single.error(Throwable())
Mockito.`when`(apiService.makeRequest()).thenReturn(single)
retroRXViewModel.fetchRetroInfo()
Assert.assertEquals(loading,retroRXViewModel.loading.value)
//Assert.assertNull(object : Any??)
}
}
Link to Code:
Dependencies:
testImplementation "org.mockito:mockito-core:3.3.3"
testImplementation 'androidx.arch.core:core-testing:2.1.0'
def rxJava_version = "2.2.19"
def rxAndroid_version = "2.1.1"
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroid_version"
implementation "io.reactivex.rxjava2:rxjava:$rxJava_version"
def retrofit_version = "2.6.1"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:retrofit-converters:$retrofit_version"
implementation "com.squareup.retrofit2:retrofit-adapters:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"