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
Search the entire web