From d7c91d77c0321425d230b605ba906a170105b73b Mon Sep 17 00:00:00 2001 From: Apple Date: Fri, 2 Jun 2023 21:56:02 +0530 Subject: [PATCH 1/2] Add fake android default repository --- .../FakeAndroidDefaultTaskRepository.kt | 126 ++++++++++++++++++ .../data/source/FakeDefaultTasksRepository.kt | 4 + 2 files changed, 130 insertions(+) create mode 100644 app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidDefaultTaskRepository.kt diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidDefaultTaskRepository.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidDefaultTaskRepository.kt new file mode 100644 index 00000000..dea0fbd3 --- /dev/null +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidDefaultTaskRepository.kt @@ -0,0 +1,126 @@ +package com.example.android.architecture.blueprints.todoapp.data.source + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.map +import com.example.android.architecture.blueprints.todoapp.data.Result +import com.example.android.architecture.blueprints.todoapp.data.Task +import kotlinx.coroutines.runBlocking + +class FakeAndroidDefaultTaskRepository : IDefaultTasksRepository { + + var tasksServiceData: LinkedHashMap = LinkedHashMap() + + private var shouldReturnError = false + + private val observableTasks = MutableLiveData>>() + + fun setReturnError(value: Boolean) { + shouldReturnError = value + } + + override suspend fun refreshTasks() { + observableTasks.value = getTasks() + } + + override suspend fun refreshTask(taskId: String) { + refreshTasks() + } + + override suspend fun updateTasksFromRemoteDataSource() { + TODO("Not yet implemented") + } + + override fun observeTasks(): LiveData>> { + runBlocking { refreshTasks() } + return observableTasks + } + + override fun observeTask(taskId: String): LiveData> { + runBlocking { refreshTasks() } + return observableTasks.map { tasks -> + when (tasks) { + is Result.Loading -> Result.Loading + is Result.Error -> Result.Error(tasks.exception) + is Result.Success -> { + val task = tasks.data.firstOrNull { it.id == taskId } + ?: return@map Result.Error(Exception("Not found")) + Result.Success(task) + } + } + } + } + + override suspend fun updateTaskFromRemoteDataSource(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result { + if (shouldReturnError) { + return Result.Error(Exception("Test exception")) + } + tasksServiceData[taskId]?.let { + return Result.Success(it) + } + return Result.Error(Exception("Could not find task")) + } + + override suspend fun getTasks(forceUpdate: Boolean): Result> { + if (shouldReturnError) { + return Result.Error(Exception("Test exception")) + } + return Result.Success(tasksServiceData.values.toList()) + } + + override suspend fun saveTask(task: Task) { + tasksServiceData[task.id] = task + } + + override suspend fun completeTask(task: Task) { + val completedTask = Task(task.title, task.description, true, task.id) + tasksServiceData[task.id] = completedTask + } + + override suspend fun completeTask(taskId: String) { + // Not required for the remote data source. + throw NotImplementedError() + } + + override suspend fun activateTask(task: Task) { + val activeTask = Task(task.title, task.description, false, task.id) + tasksServiceData[task.id] = activeTask + } + + override suspend fun activateTask(taskId: String) { + throw NotImplementedError() + } + + override suspend fun clearCompletedTasks() { + tasksServiceData = tasksServiceData.filterValues { + !it.isCompleted + } as LinkedHashMap + } + + override suspend fun deleteTask(taskId: String) { + tasksServiceData.remove(taskId) + refreshTasks() + } + + override suspend fun getTaskWithId(id: String): Result { + TODO("Not yet implemented") + } + + override suspend fun deleteAllTasks() { + tasksServiceData.clear() + refreshTasks() + } + + + fun addTasks(vararg tasks: Task) { + for (task in tasks) { + tasksServiceData[task.id] = task + } + runBlocking { refreshTasks() } + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt index df2bc4db..3972060c 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt @@ -78,6 +78,10 @@ class FakeDefaultTasksRepository : IDefaultTasksRepository { TODO("Not yet implemented") } + override suspend fun saveTask(task: Task) { + TODO("Not yet implemented") + } + fun addTask(vararg tasks: Task) { tasks.forEach { task -> tasksData[task.id] = task From c2de13861f2311b300f41635d13569eec61f07e8 Mon Sep 17 00:00:00 2001 From: Apple Date: Fri, 2 Jun 2023 21:56:02 +0530 Subject: [PATCH 2/2] Add fake android default repository Add fake android default repository --- .../FakeAndroidDefaultTaskRepository.kt | 126 ++++++++++++++++++ .../taskdetail/TaskDetailFragmentTest.kt | 26 +++- .../blueprints/todoapp/ServiceLocator.kt | 22 ++- .../todoapp/taskdetail/TaskDetailFragment.kt | 2 + .../data/source/FakeDefaultTasksRepository.kt | 4 + 5 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidDefaultTaskRepository.kt diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidDefaultTaskRepository.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidDefaultTaskRepository.kt new file mode 100644 index 00000000..dea0fbd3 --- /dev/null +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidDefaultTaskRepository.kt @@ -0,0 +1,126 @@ +package com.example.android.architecture.blueprints.todoapp.data.source + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.map +import com.example.android.architecture.blueprints.todoapp.data.Result +import com.example.android.architecture.blueprints.todoapp.data.Task +import kotlinx.coroutines.runBlocking + +class FakeAndroidDefaultTaskRepository : IDefaultTasksRepository { + + var tasksServiceData: LinkedHashMap = LinkedHashMap() + + private var shouldReturnError = false + + private val observableTasks = MutableLiveData>>() + + fun setReturnError(value: Boolean) { + shouldReturnError = value + } + + override suspend fun refreshTasks() { + observableTasks.value = getTasks() + } + + override suspend fun refreshTask(taskId: String) { + refreshTasks() + } + + override suspend fun updateTasksFromRemoteDataSource() { + TODO("Not yet implemented") + } + + override fun observeTasks(): LiveData>> { + runBlocking { refreshTasks() } + return observableTasks + } + + override fun observeTask(taskId: String): LiveData> { + runBlocking { refreshTasks() } + return observableTasks.map { tasks -> + when (tasks) { + is Result.Loading -> Result.Loading + is Result.Error -> Result.Error(tasks.exception) + is Result.Success -> { + val task = tasks.data.firstOrNull { it.id == taskId } + ?: return@map Result.Error(Exception("Not found")) + Result.Success(task) + } + } + } + } + + override suspend fun updateTaskFromRemoteDataSource(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result { + if (shouldReturnError) { + return Result.Error(Exception("Test exception")) + } + tasksServiceData[taskId]?.let { + return Result.Success(it) + } + return Result.Error(Exception("Could not find task")) + } + + override suspend fun getTasks(forceUpdate: Boolean): Result> { + if (shouldReturnError) { + return Result.Error(Exception("Test exception")) + } + return Result.Success(tasksServiceData.values.toList()) + } + + override suspend fun saveTask(task: Task) { + tasksServiceData[task.id] = task + } + + override suspend fun completeTask(task: Task) { + val completedTask = Task(task.title, task.description, true, task.id) + tasksServiceData[task.id] = completedTask + } + + override suspend fun completeTask(taskId: String) { + // Not required for the remote data source. + throw NotImplementedError() + } + + override suspend fun activateTask(task: Task) { + val activeTask = Task(task.title, task.description, false, task.id) + tasksServiceData[task.id] = activeTask + } + + override suspend fun activateTask(taskId: String) { + throw NotImplementedError() + } + + override suspend fun clearCompletedTasks() { + tasksServiceData = tasksServiceData.filterValues { + !it.isCompleted + } as LinkedHashMap + } + + override suspend fun deleteTask(taskId: String) { + tasksServiceData.remove(taskId) + refreshTasks() + } + + override suspend fun getTaskWithId(id: String): Result { + TODO("Not yet implemented") + } + + override suspend fun deleteAllTasks() { + tasksServiceData.clear() + refreshTasks() + } + + + fun addTasks(vararg tasks: Task) { + for (task in tasks) { + tasksServiceData[task.id] = task + } + runBlocking { refreshTasks() } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragmentTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragmentTest.kt index 58f2018f..d0ae58ed 100644 --- a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragmentTest.kt +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragmentTest.kt @@ -4,19 +4,41 @@ import androidx.fragment.app.testing.launchFragmentInContainer import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.example.android.architecture.blueprints.todoapp.R +import com.example.android.architecture.blueprints.todoapp.ServiceLocator import com.example.android.architecture.blueprints.todoapp.data.Task +import com.example.android.architecture.blueprints.todoapp.data.source.FakeAndroidDefaultTaskRepository +import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@ExperimentalCoroutinesApi @MediumTest @RunWith(AndroidJUnit4::class) internal class TaskDetailFragmentTest { + private lateinit var repository: IDefaultTasksRepository + + @Before + fun initRepository() { + repository = FakeAndroidDefaultTaskRepository() + ServiceLocator.tasksRepository = repository + } + + @After + fun cleanupDb() = runBlockingTest { + ServiceLocator.resetRepository() + } + @Test - fun activeTaskDetails_DisplayedInUi() { + fun activeTaskDetails_DisplayedInUi() = runBlockingTest { // Assign - Add active (incomplete) task to the DB val activeTask = Task("Active Task", "AndroidX Rocks", false) - + repository.saveTask(activeTask) + // Act - Details fragment launched to display task val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle() launchFragmentInContainer(bundle, R.style.AppTheme) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt index 7de2c64e..11306190 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt @@ -1,6 +1,7 @@ package com.example.android.architecture.blueprints.todoapp import android.content.Context +import androidx.annotation.VisibleForTesting import androidx.room.Room import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository @@ -8,13 +9,32 @@ import com.example.android.architecture.blueprints.todoapp.data.source.TasksData import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksLocalDataSource import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase import com.example.android.architecture.blueprints.todoapp.data.source.remote.TasksRemoteDataSource +import kotlinx.coroutines.runBlocking object ServiceLocator { + private val lock = Any() private var database: ToDoDatabase? = null @Volatile - private var tasksRepository: IDefaultTasksRepository? = null + var tasksRepository: IDefaultTasksRepository? = null + @VisibleForTesting set + + @VisibleForTesting + fun resetRepository() { + synchronized(lock) { + runBlocking { + TasksRemoteDataSource.deleteAllTasks() + } + // clear all data to avoid test pollution + database?.apply { + clearAllTables() + close() + } + database = null + tasksRepository = null + } + } fun provideTaskRepository(context: Context): IDefaultTasksRepository { diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt index 0f69fa49..5c2e745d 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt @@ -25,6 +25,7 @@ import com.example.android.architecture.blueprints.todoapp.EventObserver import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.TodoApplication import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository +import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository import com.example.android.architecture.blueprints.todoapp.databinding.TaskdetailFragBinding import com.example.android.architecture.blueprints.todoapp.tasks.DELETE_RESULT_OK import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout @@ -36,6 +37,7 @@ import com.google.android.material.snackbar.Snackbar */ class TaskDetailFragment : Fragment() { private lateinit var viewDataBinding: TaskdetailFragBinding + private lateinit var repository: IDefaultTasksRepository private val args: TaskDetailFragmentArgs by navArgs() diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt index df2bc4db..3972060c 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt @@ -78,6 +78,10 @@ class FakeDefaultTasksRepository : IDefaultTasksRepository { TODO("Not yet implemented") } + override suspend fun saveTask(task: Task) { + TODO("Not yet implemented") + } + fun addTask(vararg tasks: Task) { tasks.forEach { task -> tasksData[task.id] = task