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: Observerlateinit 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