Unit test, Retrofit 2, Rxjava 2 and LiveData in Android

The ViewModel to be unit tested. It has a property of MutableLiveData for holding the response data from a network api. In this example, it fetches a github account data from the github api, when the api returns with a response, the MutableLiveData is set with the response body. Whoever observes this Mutable live data will gets this new value from the network response. GithubActivityViewModel.kt

import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel
import com.example.myapplication.data.GithubAccount
import com.example.myapplication.data.GithubApi
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.observers.DisposableObserver
import io.reactivex.schedulers.Schedulers
import retrofit2.Response

class GithubActivityViewModel(private val githubApi: GithubApi) : ViewModel() {
    private val compositeDisposable = CompositeDisposable()
    var githubAccount = MutableLiveData()

    internal fun fetchGithubAccountInfo(username: String) {
        val disposable = githubApi.getGithubAccountObservable(username)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(object : DisposableObserver>() {
                    override fun onNext(response: Response) {
                        githubAccount.value = response.body()
                    }

                    override fun onComplete() {}

                    override fun onError(e: Throwable) {
                        e.printStackTrace()
                    }
                })
        compositeDisposable.add(disposable)
    }

    // This is called by the Android Activity when the activity is destroyed
    public override fun onCleared() {
        Log.d("GithubActivityViewModel", "onCleared()")
        compositeDisposable.dispose()
        super.onCleared()
    }

}

The unit test file for unit testing the above ViewModel. GithubActivityViewModelTest.kt

import android.arch.core.executor.testing.InstantTaskExecutorRule
import android.arch.lifecycle.Observer
import com.example.myapplication.data.GithubAccount
import com.example.myapplication.data.GithubApi
import org.junit.Before
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnitRunner
import io.reactivex.Observable
import retrofit2.Response

@RunWith(MockitoJUnitRunner::class)
class GithubActivityViewModelTest {

    // 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 githubApi: GithubApi

    @Mock
    lateinit var observer: Observer

    lateinit var githubFragmentViewModel: GithubActivityViewModel


    @Before
    fun setUp() {
        // initialize the ViewModed with a mocked github api
        githubFragmentViewModel = GithubActivityViewModel(githubApi)
    }

    @Test
    fun shouldShowGithubAccountLoginName() {
        // mock data
        val githubAccountName = "google"
        val githubAccount = GithubAccount(githubAccountName)

        // make the github api to return mock data
        Mockito.`when`(githubApi!!.getGithubAccountObservable(githubAccountName))
                .thenReturn(Observable.just(Response.success(githubAccount)))

        // observe on the MutableLiveData with an observer
        githubFragmentViewModel.githubAccount.observeForever(observer!!)
        githubFragmentViewModel.fetchGithubAccountInfo(githubAccountName)

        // assert that the name matches
        assert(githubFragmentViewModel.githubAccount.value!!.login == githubAccountName)
    }

}

The test rule for making the Rxjava synchronously in unit tests.RxImmediateSchedulerRule.kt

import io.reactivex.Scheduler
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.disposables.Disposable
import io.reactivex.internal.schedulers.ExecutorScheduler
import io.reactivex.plugins.RxJavaPlugins
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit

class RxImmediateSchedulerRule : TestRule {
    private val immediate = object : Scheduler() {
        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 createWorker(): Scheduler.Worker {
            return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
        }
    }

    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()
                }
            }
        }
    }
}

Other related files:

GithubActivity.kt, the github activity file which observes the GithubActivityViewModel and updates the ui when the live data in the ViewModel is updated.

import android.arch.lifecycle.Observer
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.example.myapplication.data.GithubAccount
import com.example.myapplication.data.Network
import kotlinx.android.synthetic.main.activity_github.*

class GithubActivity : AppCompatActivity() {

    lateinit var viewModel : GithubActivityViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_github)

        val factory = GithubActivityViewModelFactory(Network.getGitHubApi(this))
        viewModel = ViewModelProviders.of(this, factory).get(GithubActivityViewModel::class.java!!)
        viewModel.fetchGithubAccountInfo("google")

        initObserver()
    }

    private fun initObserver() {
        val gitHubAccountObserver =
                Observer { githubAccount ->
                    tv_account_info.text = githubAccount!!.login + "\n" + githubAccount.createdAt
                }

        viewModel.githubAccount.observe(this, gitHubAccountObserver)
    }

    // this is for passing the constructor parameter into the ViewModel
    class GithubActivityViewModelFactory(private val githubApi: GithubApi) : ViewModelProvider.Factory {
        override fun  create(modelClass: Class): T {
            if (modelClass.isAssignableFrom(GithubActivityViewModel::class.java)) {
                return GithubActivityViewModel(githubApi) as T
            }
            throw IllegalArgumentException("Unknown ViewModel class")
        }
    }
}

app gradle file

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.example.myapplication"
        minSdkVersion 16
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    testOptions {
        unitTests.returnDefaultValues = true
    }

    packagingOptions {
        exclude 'META-INF/rxjava.properties'
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'

    // test dependencies
    testImplementation 'junit:junit:4.12'
    testImplementation "org.mockito:mockito-core:2.+"
    testImplementation 'android.arch.core:core-testing:1.0.0-rc1'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'

    // android architecture component library
    implementation 'android.arch.lifecycle:extensions:1.0.0-rc1'

    // rxandroid
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

    // Retrofit 2 for network tasks
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
    implementation 'com.google.code.gson:gson:2.8.2'
}

GithubAccount.kt, this is a pojo file for holding api response after it is converted by the Gson converter.

import com.google.gson.annotations.SerializedName

data class GithubAccount(
        @SerializedName("login") var login : String = "",
        @SerializedName("id") var id : Int = 0,
        @SerializedName("avatar_url") var avatarUrl : String = "",
        @SerializedName("created_at") var createdAt : String = "",
        @SerializedName("updated_at") var updatedAt : String = "") {

    override fun equals(obj: Any?): Boolean {
        return login == (obj as GithubAccount).login
    }
}

GithubApi.kt, the Retrofit network interface file.

import io.reactivex.Observable
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path

interface GithubApi {
    @GET("/users/{username}")
    fun getGithubAccountObservable(@Path("username") username: String): Observable>
}

Network.kt, the Retrofit network api factory file.

import android.content.Context
import android.util.Log
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import okhttp3.CacheControl
import okhttp3.Interceptor
import java.util.concurrent.TimeUnit

object Network {

    fun getGitHubApi(context: Context) : GithubApi {

        val cache = createCache(context)
        val networkCacheInterceptor = createCacheInterceptor()
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY


        val httpClient = OkHttpClient.Builder()
                .cache(cache)
                .addNetworkInterceptor(networkCacheInterceptor)
                .addInterceptor(loggingInterceptor)
                .build()

        val retrofit = Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(httpClient)
                .build()

        return retrofit.create(GithubApi::class.java)
    }


    private fun createCache(context: Context): Cache? {
        var cache: Cache? = null
        try {
            val cacheSize = 10 * 1024 * 1024 // 10 MB
            val httpCacheDirectory = File(context.getCacheDir(), "http-cache")
            cache = Cache(httpCacheDirectory, cacheSize.toLong())
        } catch (e: Exception) {
            e.printStackTrace()
            Log.e("error", "Failed to create create Cache!")
        }

        return cache
    }


    private fun createCacheInterceptor(): Interceptor {
        return Interceptor { chain ->
            val response = chain.proceed(chain.request())

            var cacheControl = CacheControl.Builder()
                    .maxAge(1, TimeUnit.MINUTES)
                    .build()

            response.newBuilder()
                    .header("Cache-Control", cacheControl.toString())
                    .build()
        }
    }

}

MainActivity.kt, the main activity file which launches on app launch.

import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.View
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun launchGithub(v: View) {
        startActivity(Intent(this, GithubActivity::class.java))
    }

}

References:
http://square.github.io/retrofit/
https://developer.android.com/topic/libraries/architecture/livedata.html
https://github.com/ReactiveX/RxJava
https://developer.android.com/reference/android/arch/core/executor/testing/InstantTaskExecutorRule.html
https://stackoverflow.com/questions/43356314/android-rxjava-2-junit-test-getmainlooper-in-android-os-looper-not-mocked-runt/43356315

Search within Codexpedia

Custom Search

Search the entire web

Custom Search