Skip to content

Commit

Permalink
Added Unit testing with Mockito
Browse files Browse the repository at this point in the history
  • Loading branch information
emedinaa committed May 27, 2019
1 parent a9fab29 commit 92d9e5a
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 50 deletions.
8 changes: 6 additions & 2 deletions KotlinMVVM/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@ dependencies {
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
implementation "androidx.cardview:cardview:$cardViewVersion"

testImplementation "junit:junit:$junitVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "android.arch.core:core-testing:$archTestingVersion"

androidTestImplementation "androidx.test:runner:$runnerVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

implementation "androidx.lifecycle:lifecycle-extensions:$archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$archLifecycleVersion"
Expand All @@ -44,6 +49,5 @@ dependencies {
implementation "com.google.code.gson:gson:$rootProject.gsonVersion"
implementation "com.squareup.retrofit2:converter-gson:$rootProject.gson"
implementation "com.squareup.okhttp3:logging-interceptor:$rootProject.okhttp3"

implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation "com.github.bumptech.glide:glide:$rootProject.glideVersion"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ package com.emedinaa.kotlinmvvm.data

import com.emedinaa.kotlinmvvm.model.Museum

data class MuseumResponse(val status:Int?,val msg:String?,val data:List<Museum>?)
data class MuseumResponse(val status:Int?,val msg:String?,val data:List<Museum>?){
fun isSuccess():Boolean= (status==200)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.emedinaa.kotlinmvvm.data

interface OperationCallback {

fun onSuccess(obj:Any?)
fun onError(obj:Any?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import com.emedinaa.kotlinmvvm.model.MuseumRepository

object Injection {

//MuseumRepository could be a singleton
fun providerRepository():MuseumDataSource{
return MuseumRepository()//could be a singleton
return MuseumRepository()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@ import com.emedinaa.kotlinmvvm.data.OperationCallback
interface MuseumDataSource {

fun retrieveMuseums(callback: OperationCallback)

fun cancel()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

const val TAG="CONSOLE"

class MuseumRepository:MuseumDataSource {

private var call:Call<MuseumResponse>?=null
Expand All @@ -21,8 +23,8 @@ class MuseumRepository:MuseumDataSource {

override fun onResponse(call: Call<MuseumResponse>, response: Response<MuseumResponse>) {
response?.body()?.let {
if(response.isSuccessful && (it.status==200)){ //200
Log.v("CONSOLE", "data ${it.data}")
if(response.isSuccessful && (it.isSuccess())){
Log.v(TAG, "data ${it.data}")
callback.onSuccess(it.data)
}else{
callback.onError(it.msg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,44 @@ class MuseumActivity : AppCompatActivity() {
const val TAG= "CONSOLE"
}

/**
//Consider this, if you need to call the service once when activity was created.
Log.v(TAG,"savedInstanceState $savedInstanceState")
if(savedInstanceState==null){
viewModel.loadMuseums()
}
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_museum)

//viewmodel
setUpViewModel()
setupViewModel()
setupUI()
}

//ui
//ui
private fun setupUI(){
adapter= MuseumAdapter(viewModel.museums.value?: emptyList())
recyclerView.layoutManager= LinearLayoutManager(this)
recyclerView.adapter= adapter

Log.v(TAG,"savedInstanceState $savedInstanceState")

//Consider this, if you need to call the service once when activity was created.
/*if(savedInstanceState==null){
viewModel.loadMuseums()
}*/
}

private fun setUpViewModel(){
//viewModel = ViewModelProviders.of(this).get(MuseumViewModel::class.java)
//viewmodel
/**
//Consider this if ViewModel class don't require parameters.
viewModel = ViewModelProviders.of(this).get(MuseumViewModel::class.java)
//if you require any parameters to the ViewModel consider use a ViewModel Factory
viewModel = ViewModelProviders.of(this,ViewModelFactory(Injection.providerRepository())).get(MuseumViewModel::class.java)
/*viewModel.museums.observe(this,
Observer<List<Museum>> {
Log.v("CONSOLE", "data updated $it")
adapter.update(it)
})*/
//Anonymous observer implementation
viewModel.museums.observe(this,Observer<List<Museum>> {
Log.v("CONSOLE", "data updated $it")
adapter.update(it)
})
*/
private fun setupViewModel(){
viewModel = ViewModelProviders.of(this,ViewModelFactory(Injection.providerRepository())).get(MuseumViewModel::class.java)
viewModel.museums.observe(this,renderMuseums)

viewModel.isViewLoading.observe(this,isViewLoadingObserver)
Expand Down Expand Up @@ -88,11 +96,11 @@ class MuseumActivity : AppCompatActivity() {
layoutError.visibility=View.GONE
}

/**
* If you require updated data, you can call the method "loadMuseum" here
*/
override fun onResume() {

//If you require updated data, you can call the method "loadMuseum" here
override fun onResume() {
super.onResume()
//viewModel.loadMuseums()
}
viewModel.loadMuseums()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class MuseumViewModel(private val repository: MuseumDataSource):ViewModel() {
"loadMuseums()" on constructor. Also, if you rotate the screen, the service will not be called.
*/
init {
loadMuseums()
//loadMuseums()
}

fun loadMuseums(){
Expand All @@ -40,7 +40,7 @@ class MuseumViewModel(private val repository: MuseumDataSource):ViewModel() {
override fun onSuccess(obj: Any?) {
_isViewLoading.postValue(false)

if(obj is List<*>){
if(obj!=null && obj is List<*>){
if(obj.isEmpty()){
_isEmptyList.postValue(true)
}else{
Expand Down

This file was deleted.

124 changes: 124 additions & 0 deletions KotlinMVVM/app/src/test/java/com/emedinaa/kotlinmvvm/MVVMUnitTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.emedinaa.kotlinmvvm

import android.app.Application
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import com.emedinaa.kotlinmvvm.data.OperationCallback
import com.emedinaa.kotlinmvvm.model.Museum
import com.emedinaa.kotlinmvvm.model.MuseumDataSource
import com.emedinaa.kotlinmvvm.viewmodel.MuseumViewModel
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.*
import org.mockito.Mockito.*

class MVVMUnitTest {

@Mock
private lateinit var repository: MuseumDataSource
@Mock private lateinit var context: Application

@Captor
private lateinit var operationCallbackCaptor: ArgumentCaptor<OperationCallback>

private lateinit var viewModel:MuseumViewModel

private lateinit var isViewLoadingObserver:Observer<Boolean>
private lateinit var onMessageErrorObserver:Observer<Any>
private lateinit var emptyListObserver:Observer<Boolean>
private lateinit var onRenderMuseumsObserver:Observer<List<Museum>>

private lateinit var museumEmptyList:List<Museum>
private lateinit var museumList:List<Museum>

/**
//https://jeroenmols.com/blog/2019/01/17/livedatajunit5/
//Method getMainLooper in android.os.Looper not mocked
java.lang.IllegalStateException: operationCallbackCaptor.capture() must not be null
*/
@get:Rule
val rule = InstantTaskExecutorRule()

@Before
fun setup() {
MockitoAnnotations.initMocks(this)
`when`<Context>(context.applicationContext).thenReturn(context)

viewModel= MuseumViewModel(repository)

mockData()
setupObservers()
}

@Test
fun museumEmptyListRepositoryAndViewModel(){
with(viewModel){
loadMuseums()
isViewLoading.observeForever(isViewLoadingObserver)
//onMessageError.observeForever(onMessageErrorObserver)
isEmptyList.observeForever(emptyListObserver)
museums.observeForever(onRenderMuseumsObserver)
}

verify(repository, times(1)).retrieveMuseums(capture(operationCallbackCaptor))
operationCallbackCaptor.value.onSuccess(museumEmptyList)

Assert.assertNotNull(viewModel.isViewLoading.value)
//Assert.assertNotNull(viewModel.onMessageError.value) //java.lang.AssertionError
//Assert.assertNotNull(viewModel.isEmptyList.value)
Assert.assertTrue(viewModel.isEmptyList.value==true)
Assert.assertTrue(viewModel.museums.value?.size==0)
}

@Test
fun museumListRepositoryAndViewModel(){
with(viewModel){
loadMuseums()
isViewLoading.observeForever(isViewLoadingObserver)
museums.observeForever(onRenderMuseumsObserver)
}

verify(repository, times(1)).retrieveMuseums(capture(operationCallbackCaptor))
operationCallbackCaptor.value.onSuccess(museumList)

Assert.assertNotNull(viewModel.isViewLoading.value)
Assert.assertTrue(viewModel.museums.value?.size==3)
}

@Test
fun museumFailRepositoryAndViewModel(){
with(viewModel){
loadMuseums()
isViewLoading.observeForever(isViewLoadingObserver)
onMessageError.observeForever(onMessageErrorObserver)
}
verify(repository, times(1)).retrieveMuseums(capture(operationCallbackCaptor))
operationCallbackCaptor.value.onError("Ocurrió un error")
Assert.assertNotNull(viewModel.isViewLoading.value)
Assert.assertNotNull(viewModel.onMessageError.value)
}

private fun setupObservers(){
isViewLoadingObserver= mock(Observer::class.java) as Observer<Boolean>
onMessageErrorObserver= mock(Observer::class.java) as Observer<Any>
emptyListObserver= mock(Observer::class.java) as Observer<Boolean>
onRenderMuseumsObserver= mock(Observer::class.java)as Observer<List<Museum>>
}

private fun mockData(){
museumEmptyList= emptyList()
val mockList:MutableList<Museum> = mutableListOf()
mockList.add(Museum(0,"Museo Nacional de Arqueología, Antropología e Historia del Perú",""))
mockList.add(Museum(1,"Museo de Sitio Pachacamac",""))
mockList.add(Museum(2,"Casa Museo José Carlos Mariátegui",""))

museumList= mockList.toList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.emedinaa.kotlinmvvm

import org.mockito.ArgumentCaptor

/**
* Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
* when null is returned.
*/
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
3 changes: 3 additions & 0 deletions KotlinMVVM/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ ext {
rulesVersion = '1.0.1'
espressoVersion = '3.1.1'
junitVersion = '4.12'
mockitoVersion= '2.27.0' //https://site.mockito.org/
archTestingVersion = '1.1.1'

gsonVersion='2.8.0'
retrofit2='2.3.0'
gson='2.3.0'
okhttp3='3.4.1'
glideVersion='4.9.0'
}
Loading

0 comments on commit 92d9e5a

Please sign in to comment.