소스 검색

add unit-tests for data layer person feature

MrOzOn 5 년 전
부모
커밋
d673094e34

+ 5 - 2
core_api/src/main/java/com/mrozon/core_api/SingleSourceOfTruthStrategy.kt

@@ -5,11 +5,14 @@ import androidx.lifecycle.liveData
 import androidx.lifecycle.map
 import kotlinx.coroutines.Dispatchers
 import com.mrozon.utils.network.Result
+import kotlin.coroutines.CoroutineContext
 
-fun <T, A> resultLiveData(databaseQuery: () -> LiveData<T>,
+fun <T, A> resultLiveData(
+                          coroutineContext: CoroutineContext,
+                          databaseQuery: () -> LiveData<T>,
                           networkCall: suspend () -> Result<A>,
                           saveCallResult: suspend (A) -> Unit): LiveData<Result<T>> =
-        liveData(Dispatchers.IO) {
+        liveData(coroutineContext) {
             emit(Result.loading<T>())
             val source = databaseQuery.invoke().map { Result.success(it) }
             emitSource(source)

+ 8 - 1
core_api/src/main/java/com/mrozon/core_api/db/HealthDiaryDao.kt

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
 import androidx.room.*
 import com.mrozon.core_api.db.model.PersonDb
 import com.mrozon.core_api.db.model.UserDb
+import kotlinx.coroutines.flow.Flow
 
 @Dao
 interface HealthDiaryDao {
@@ -24,7 +25,7 @@ interface HealthDiaryDao {
 
     // PERSON
     @Query("SELECT * FROM person_table")
-    fun getPersons(): LiveData<List<PersonDb>>
+    fun getPersons(): Flow<List<PersonDb>>
 
     @Query("SELECT * FROM person_table WHERE person_id=:id LIMIT 1")
     suspend fun getPerson(id: Long): PersonDb
@@ -40,4 +41,10 @@ interface HealthDiaryDao {
 
     @Query("DELETE FROM person_table")
     suspend fun deleteAllPerson()
+
+    @Transaction
+    suspend fun reloadPersons(persons: List<PersonDb>) {
+        deleteAllPerson()
+        insertAllPerson(persons)
+    }
 }

+ 1 - 1
core_api/src/main/java/com/mrozon/core_api/db/model/PersonDb.kt

@@ -21,5 +21,5 @@ data class PersonDb(
     var gender: Int = 0,
 
     @ColumnInfo(name = "user_born")
-    var born: Date
+    var born: Date = Date()
 )

+ 2 - 2
core_api/src/main/java/com/mrozon/core_api/entity/Person.kt

@@ -4,9 +4,9 @@ import java.util.Date
 
 data class Person (
     val id: Long = 0L,
-    val name: String,
+    val name: String = "",
     val gender: Gender = Gender.MALE,
-    val born: Date
+    val born: Date = Date()
 )
 
 enum class Gender(val code: Int) {

+ 2 - 0
feature_person/build.gradle

@@ -64,6 +64,8 @@ dependencies {
     implementation cardview
     implementation recyclerview
     implementation material
+    //livaData
+    implementation lifecycleLivedata
     //Unit test
     testImplementation junit
     testImplementation mockitoCore

+ 2 - 1
feature_person/src/main/java/com/mrozon/feature_person/data/PersonRepository.kt

@@ -7,7 +7,8 @@ import com.mrozon.utils.network.Result
 import kotlinx.coroutines.flow.Flow
 
 interface PersonRepository {
-    fun getPersons(): LiveData<Result<List<Person>>>
+    fun getPersons(): Flow<Result<List<Person>>>
+    fun refreshPersons(): Flow<Result<List<Person>>>
     fun addPerson(person: Person): Flow<Result<Person>>
     fun getPerson(id: Long): Flow<Result<Person>>
     fun deletePerson(id: Long): Flow<Result<Unit>>

+ 62 - 17
feature_person/src/main/java/com/mrozon/feature_person/data/PersonRepositoryImpl.kt

@@ -10,14 +10,14 @@ import com.mrozon.core_api.network.model.PersonRequest
 import com.mrozon.core_api.network.model.PersonResponse
 import com.mrozon.core_api.network.model.SharePersonRequest
 import com.mrozon.core_api.network.model.toPerson
+import com.mrozon.core_api.providers.CoroutineContextProvider
 import com.mrozon.core_api.resultLiveData
 import com.mrozon.utils.extension.toDateString
 import com.mrozon.utils.network.Result
 import com.mrozon.utils.network.Result.Companion.error
 import com.mrozon.utils.network.Result.Companion.loading
 import com.mrozon.utils.network.Result.Companion.success
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.*
 import retrofit2.Response
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -28,23 +28,67 @@ class PersonRepositoryImpl @Inject constructor(private val dao: HealthDiaryDao,
         private val mapper: PersonToPersonDbMapper
 ): PersonRepository {
 
-    override fun getPersons() = resultLiveData(
-        databaseQuery = {
-            val personDb = dao.getPersons()
-            Transformations.map(personDb) {
-                mapper.reverseMap(it)
+    override fun getPersons(): Flow<Result<List<Person>>> {
+        return flow {
+            emit(loading())
+            val query = dao.getPersons()
+            val result = query.firstOrNull()
+            result?.let {
+                emit(success(mapper.reverseMap(it)))
             }
-        },
-        networkCall = { personRemoteDataSource.getPersons() },
-        saveCallResult = {
-            val persons = it.map { personResponse ->
-                personResponse.toPerson()
+            val networkResult = personRemoteDataSource.getPersons()
+            if (networkResult.status == Result.Status.SUCCESS) {
+                val data = networkResult.data!!
+                val persons = data.map { personResponse ->
+                    personResponse.toPerson()
+                }
+                val personsDb = mapper.map(persons)
+                dao.reloadPersons(personsDb)
+                emit(success(persons))
+            } else if (networkResult.status == Result.Status.ERROR) {
+                emit(error(networkResult.message!!))
+            }
+        }
+    }
+
+    override fun refreshPersons(): Flow<Result<List<Person>>> {
+        return flow {
+            emit(loading())
+            val networkResult = personRemoteDataSource.getPersons()
+            if (networkResult.status == Result.Status.SUCCESS) {
+                val data = networkResult.data!!
+                val persons = data.map { personResponse ->
+                    personResponse.toPerson()
+                }
+                val personsDb = mapper.map(persons)
+                dao.reloadPersons(personsDb)
+                emit(success(persons))
+            } else if (networkResult.status == Result.Status.ERROR) {
+                emit(error(networkResult.message!!))
             }
-            val personsDb = mapper.map(persons)
-//            dao.deleteAllPerson()
-            dao.insertAllPerson(personsDb)
         }
-    )
+    }
+
+//    override fun getPersons() = resultLiveData(
+//        coroutineContext = coroutineContextProvider.IO,
+//        databaseQuery = {
+//            val personDb = dao.getPersons()
+//            Transformations.map(personDb) {
+//                mapper.reverseMap(it)
+//            }
+//        },
+//        networkCall = { personRemoteDataSource.getPersons() },
+//        saveCallResult = {
+//            val persons = it.map { personResponse ->
+//                personResponse.toPerson()
+//            }
+//            val personsDb = mapper.map(persons)
+////            dao.deleteAllPerson()
+//            dao.insertAllPerson(personsDb)
+//        }
+//    )
+
+
 
     override fun addPerson(person: Person): Flow<Result<Person>> {
         return flow {
@@ -65,8 +109,8 @@ class PersonRepositoryImpl @Inject constructor(private val dao: HealthDiaryDao,
 
     override fun getPerson(id: Long): Flow<Result<Person>> {
         return flow {
+            emit(loading())
             try {
-                emit(loading())
                 val response = dao.getPerson(id)
                 emit(success(mapper.reverseMap(response)!!))
             }
@@ -78,6 +122,7 @@ class PersonRepositoryImpl @Inject constructor(private val dao: HealthDiaryDao,
 
     override fun deletePerson(id: Long): Flow<Result<Unit>> {
         return flow {
+            emit(loading())
             val response = personRemoteDataSource.deletePerson(id)
             if (response.status == Result.Status.SUCCESS) {
                 dao.deletePerson(id)

+ 44 - 13
feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonFragment.kt

@@ -3,19 +3,23 @@ package com.mrozon.feature_person.presentation
 import android.content.Context
 import android.os.Bundle
 import android.os.Handler
-import android.view.View
+import android.view.*
 import androidx.activity.addCallback
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
 import androidx.navigation.fragment.findNavController
 import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.LARGE
 import com.mrozon.core_api.entity.Person
 import com.mrozon.core_api.navigation.ListPersonNavigator
 import com.mrozon.feature_person.R
 import com.mrozon.feature_person.databinding.FragmentListPersonBinding
 import com.mrozon.feature_person.di.ListPersonFragmentComponent
 import com.mrozon.utils.base.BaseFragment
+import com.mrozon.utils.extension.hideKeyboard
+import com.mrozon.utils.extension.isActiveNetwork
 import com.mrozon.utils.extension.visible
 import com.mrozon.utils.network.Result
 import timber.log.Timber
@@ -47,6 +51,15 @@ class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
         Timber.d("onAttach")
     }
 
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        setHasOptionsMenu(true)
+        return super.onCreateView(inflater, container, savedInstanceState)
+    }
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
         adapter = ListPersonAdapter(object : ListPersonAdapter.ListPersonClickListener {
@@ -68,21 +81,39 @@ class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
         binding?.fabAddPerson?.setOnClickListener {
             navigator.navigateToEditPerson(findNavController(),getString(R.string.add_person),0)
         }
+
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.list_person_menu, menu)
+        return super.onCreateOptionsMenu(menu, inflater)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        hideKeyboard()
+        when(item.itemId){
+            R.id.refreshPersonNetwork -> {
+                viewModel.refreshPersons()
+            }
+        }
+        return false
     }
 
     override fun subscribeUi() {
-        viewModel.persons.observe(viewLifecycleOwner, Observer { result ->
-            when (result.status) {
-                Result.Status.LOADING -> {
-                    binding?.progressBar?.visible(true)
-                }
-                Result.Status.SUCCESS -> {
-                    binding?.progressBar?.visible(false)
-                    adapter.submitList(result.data)
-                }
-                Result.Status.ERROR -> {
-                    binding?.progressBar?.visible(false)
-                    showError(result.message!!)
+        viewModel.persons.observe(viewLifecycleOwner, Observer { event ->
+            event.peekContent().let { result ->
+                when (result.status) {
+                    Result.Status.LOADING -> {
+                        binding?.progressBar?.visible(true)
+                    }
+                    Result.Status.SUCCESS -> {
+                        binding?.progressBar?.visible(false)
+                        adapter.submitList(result.data)
+                    }
+                    Result.Status.ERROR -> {
+                        binding?.progressBar?.visible(false)
+                        showError(result.message!!)
+                    }
                 }
             }
         })

+ 42 - 2
feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonFragmentViewModel.kt

@@ -1,10 +1,50 @@
 package com.mrozon.feature_person.presentation
 
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.mrozon.core_api.entity.Person
+import com.mrozon.core_api.entity.User
+import com.mrozon.core_api.providers.CoroutineContextProvider
 import com.mrozon.feature_person.data.PersonRepository
+import com.mrozon.utils.Event
 import com.mrozon.utils.base.BaseViewModel
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import timber.log.Timber
 import javax.inject.Inject
 
-class ListPersonFragmentViewModel @Inject constructor(repository: PersonRepository): BaseViewModel() {
+class ListPersonFragmentViewModel @Inject constructor(
+    private val repository: PersonRepository,
+    private val coroutineContextProvider: CoroutineContextProvider
+): BaseViewModel() {
+//    val persons by lazy { repository.getPersons() }
+
+    private val _persons = MutableLiveData<Event<Result<List<Person>>>>()
+    val persons: LiveData<Event<Result<List<Person>>>>
+        get() = _persons
+
+    init {
+        Timber.d("init")
+        viewModelScope.launch(coroutineContextProvider.IO){
+            repository.getPersons().collect {
+                withContext(coroutineContextProvider.Main) {
+                    _persons.value = Event(it)
+                }
+            }
+        }
+    }
+
+    fun refreshPersons() {
+        viewModelScope.launch(coroutineContextProvider.IO){
+            repository.refreshPersons().collect {
+                withContext(coroutineContextProvider.Main) {
+                    _persons.value = Event(it)
+                }
+            }
+        }
+    }
 
-    val persons by lazy { repository.getPersons() }
 }

+ 5 - 0
feature_person/src/main/res/drawable/ic_refresh_24.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
+</vector>

+ 2 - 1
feature_person/src/main/res/layout/fragment_list_person.xml

@@ -23,7 +23,8 @@
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:srcCompat="@drawable/ic_add_24"
-            app:useCompatPadding="true" />
+            app:useCompatPadding="true"
+            tools:ignore="ContentDescription" />
 
 
         <androidx.recyclerview.widget.RecyclerView

+ 11 - 0
feature_person/src/main/res/menu/list_person_menu.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:id="@+id/refreshPersonNetwork"
+        android:icon="@drawable/ic_refresh_24"
+        android:title="@string/refresh"
+        app:showAsAction="ifRoom" />
+
+</menu>

+ 1 - 0
feature_person/src/main/res/values/strings.xml

@@ -16,4 +16,5 @@
     <string name="error_invalid_email">Invalid format username (e-mail)</string>
     <string name="share_done">Done</string>
     <string name="confirm_exit">Please click BACK again to exit</string>
+    <string name="refresh">Refresh</string>
 </resources>

+ 0 - 17
feature_person/src/test/java/com/mrozon/feature_person/ExampleUnitTest.kt

@@ -1,17 +0,0 @@
-package com.mrozon.feature_person
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
-    @Test
-    fun addition_isCorrect() {
-        assertEquals(4, 2 + 2)
-    }
-}

+ 344 - 0
feature_person/src/test/java/com/mrozon/feature_person/data/PersonRepositoryImplTest.kt

@@ -0,0 +1,344 @@
+package com.mrozon.feature_person.data
+
+import com.mrozon.core_api.db.HealthDiaryDao
+import com.mrozon.core_api.entity.User
+import com.mrozon.core_api.mapper.PersonToPersonDbMapper
+import com.mrozon.core_api.network.HealthDiaryService
+import com.mrozon.core_api.network.model.*
+import com.mrozon.core_api.providers.CoroutineContextProvider
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.*
+import okhttp3.MediaType
+import okhttp3.ResponseBody
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import retrofit2.Response
+import kotlin.coroutines.CoroutineContext
+
+@ExperimentalCoroutinesApi
+class PersonRepositoryImplTest {
+
+    @get:Rule
+    val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    lateinit var apiService: HealthDiaryService
+
+    @Mock
+    lateinit var dao: HealthDiaryDao
+
+    @get:Rule
+    var coroutinesTestRule = CoroutineTestRule()
+
+    lateinit var repository: PersonRepository
+
+    @Before
+    fun setUp() {
+        val source = PersonRemoteDataSource(apiService, dao)
+        repository = PersonRepositoryImpl(dao, source, PersonToPersonDbMapper())
+    }
+
+    @Test
+    fun `get persons success`() = coroutinesTestRule.runBlockingTest {
+        val date = "2000-10-24"
+        val response = PersonResponse(avatar = "", born = date, created_date = date,
+            gender = true, id=100, name = "", owners = listOf())
+        val person = response.toPerson()
+        val personDb = PersonToPersonDbMapper().map(person)!!
+        Mockito.`when`(apiService.getPersons()).thenReturn(
+            Response.success(listOf(response))
+        )
+        Mockito.`when`(dao.getPersons()).thenReturn(
+            flowOf(listOf(personDb))
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.success(listOf(person)),
+            Result.success(listOf(person))
+        )
+
+        val actual = repository.getPersons()
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+
+    @Test
+    fun `get persons network down success`() = coroutinesTestRule.runBlockingTest {
+        val error = "bla-bla"
+        val date = "2000-10-24"
+        val response = PersonResponse(avatar = "", born = date, created_date = date,
+            gender = true, id=100, name = "", owners = listOf())
+        val person = response.toPerson()
+        val personDb = PersonToPersonDbMapper().map(person)!!
+        Mockito.`when`(apiService.getPersons()).thenReturn(
+            Response.error(402, ResponseBody.create(MediaType.get("text/plain"),"[$error]"))
+        )
+        Mockito.`when`(dao.getPersons()).thenReturn(
+            flowOf(listOf(personDb))
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.success(listOf(person)),
+            Result.error("Network error:\n $error",null)
+        )
+
+        val actual = repository.getPersons()
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `refresh persons success`() = coroutinesTestRule.runBlockingTest {
+        val date = "2000-10-24"
+        val response = PersonResponse(avatar = "", born = date, created_date = date,
+            gender = true, id=100, name = "", owners = listOf())
+        val person = response.toPerson()
+        val personDb = PersonToPersonDbMapper().map(person)!!
+        Mockito.`when`(apiService.getPersons()).thenReturn(
+            Response.success(listOf(response))
+        )
+        Mockito.`when`(dao.getPersons()).thenReturn(
+            flowOf(listOf(personDb))
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.success(listOf(person))
+        )
+
+        val actual = repository.refreshPersons()
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `refresh persons network down success`() = coroutinesTestRule.runBlockingTest {
+        val error = "bla-bla"
+        val date = "2000-10-24"
+        val response = PersonResponse(avatar = "", born = date, created_date = date,
+            gender = true, id=100, name = "", owners = listOf())
+        val person = response.toPerson()
+        val personDb = PersonToPersonDbMapper().map(person)!!
+        Mockito.`when`(apiService.getPersons()).thenReturn(
+            Response.error(402, ResponseBody.create(MediaType.get("text/plain"),"[$error]"))
+        )
+        Mockito.`when`(dao.getPersons()).thenReturn(
+            flowOf(listOf(personDb))
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.error("Network error:\n $error",null)
+        )
+
+        val actual = repository.refreshPersons()
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `add person success`() = coroutinesTestRule.runBlockingTest {
+        val date = "2000-10-24"
+        val request = PersonRequest(born = date, gender = true, name ="")
+        val response = PersonResponse(avatar = "", born = date, created_date = date,
+            gender = true, id=100, name = "", owners = listOf())
+        val person = response.toPerson()
+        Mockito.`when`(apiService.addPerson(request)).thenReturn(
+            Response.success(response)
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.success(person)
+        )
+
+        val actual = repository.addPerson(person)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `add person failed`() = coroutinesTestRule.runBlockingTest {
+        val date = "2000-10-24"
+        val error = "bla-bla"
+        val request = PersonRequest(born = date, gender = true, name ="")
+        val response = PersonResponse(avatar = "", born = date, created_date = date,
+            gender = true, id=100, name = "", owners = listOf())
+        val person = response.toPerson()
+        Mockito.`when`(apiService.addPerson(request)).thenReturn(
+            Response.error(402, ResponseBody.create(MediaType.get("text/plain"),"[$error]"))
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.error("Network error:\n $error",null)
+        )
+
+        val actual = repository.addPerson(person)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `get person success`() = coroutinesTestRule.runBlockingTest {
+        val date = "2000-10-24"
+        val response = PersonResponse(avatar = "", born = date, created_date = date,
+            gender = true, id=100, name = "", owners = listOf())
+        val person = response.toPerson()
+        val personDb = PersonToPersonDbMapper().map(person)!!
+        Mockito.`when`(dao.getPerson(Mockito.anyLong())).thenReturn(
+            personDb
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.success(person)
+        )
+
+        val actual = repository.getPerson(1)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `delete person success`() = coroutinesTestRule.runBlockingTest {
+        Mockito.`when`(apiService.deletePerson(Mockito.anyString())).thenReturn(
+            Response.success(Unit)
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.success(null)
+        )
+
+        val actual = repository.deletePerson(1)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `delete person failed`() = coroutinesTestRule.runBlockingTest {
+        val error = "bla-bla"
+        Mockito.`when`(apiService.deletePerson(Mockito.anyString())).thenReturn(
+            Response.error(402, ResponseBody.create(MediaType.get("text/plain"),"[$error]"))
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.error("Network error:\n $error",null)
+        )
+
+        val actual = repository.deletePerson(1)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `share person success`() = coroutinesTestRule.runBlockingTest {
+        val id = 100
+        val name = "aaaaaa"
+        val date = "2014-11-11"
+        val request = SharePersonRequest(patient_id = id, username = name)
+        val response = PersonResponse(avatar = "", born = date, created_date = date,
+            gender = true, id=100, name = "", owners = listOf())
+        Mockito.`when`(apiService.sharePerson(request)).thenReturn(
+            Response.success(response)
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.success(null)
+        )
+
+        val actual = repository.sharePerson(id.toLong(),name)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `share person failed`() = coroutinesTestRule.runBlockingTest {
+        val error = "bla-bla"
+        val id = 100
+        val name = "aaaaaa"
+        val date = "2014-11-11"
+        val request = SharePersonRequest(patient_id = id, username = name)
+        val response = PersonResponse(avatar = "", born = date, created_date = date,
+            gender = true, id=100, name = "", owners = listOf())
+        Mockito.`when`(apiService.sharePerson(request)).thenReturn(
+            Response.error(402, ResponseBody.create(MediaType.get("text/plain"),"[$error]"))
+        )
+        val expected = listOf(
+            Result.loading(),
+            Result.error("Network error:\n $error",null)
+        )
+
+        val actual = repository.sharePerson(id.toLong(),name)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+
+
+
+}
+
+
+@ExperimentalCoroutinesApi
+class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : TestWatcher() {
+
+    private val testCoroutinesScope = TestCoroutineScope(testDispatcher)
+
+    override fun apply(base: Statement, description: Description?) = object : Statement() {
+        override fun evaluate() {
+            Dispatchers.setMain(testDispatcher)
+            base.evaluate()
+            Dispatchers.resetMain()
+            testCoroutinesScope.cleanupTestCoroutines()
+        }
+    }
+
+    fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
+        testCoroutinesScope.runBlockingTest{
+            block()
+        }
+    }
+}

+ 2 - 1
scripts/deps_versions.gradle

@@ -2,7 +2,7 @@ ext {
 
 
     daggerVersion = '2.25.2'
-    roomVersion = '2.1.0'
+    roomVersion = '2.2.0'
     navigationVersion = '2.3.0'
     timberVersion = '4.7.1'
     retrofitVersion = '2.6.1'
@@ -33,6 +33,7 @@ ext {
     room = "androidx.room:room-runtime:$roomVersion"
     roomKtx = "androidx.room:room-ktx:$roomVersion"
     roomCompiler = "androidx.room:room-compiler:$roomVersion"
+    roomCoroutines = "androidx.room:room-coroutines:$roomVersion"
     // Navigation
     navigationFragment = "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
     navigationUi = "androidx.navigation:navigation-ui-ktx:$navigationVersion"