From a9612ed5ed7acc8e27013e06a83408cde0d3696c Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Thu, 25 May 2023 18:52:12 +0530 Subject: [PATCH 01/15] TDD Unit test for StatisticsUtils --- app/build.gradle | 1 + .../todoapp/statistics/StatisticsUtils.kt | 24 +++-- .../todoapp/statistics/StatisticsUtilsTest.kt | 88 +++++++++++++++++++ 3 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt diff --git a/app/build.gradle b/app/build.gradle index edea69366..c95292970 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,6 +52,7 @@ dependencies { // Dependencies for local unit tests testImplementation "junit:junit:$junitVersion" + testImplementation "com.google.truth:truth:1.1.2" // AndroidX Test - Instrumented testing androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion" diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt index 968b8f144..78dc4cd28 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt @@ -22,11 +22,25 @@ import com.example.android.architecture.blueprints.todoapp.data.Task * Function that does some trivial computation. Used to showcase unit tests. */ internal fun getActiveAndCompletedStats(tasks: List?): StatsResult { - val totalTasks = tasks!!.size - val numberOfActiveTasks = tasks.count { it.isActive } - return StatsResult( - activeTasksPercent = 100f * numberOfActiveTasks / tasks.size, - completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size + + return tasks?.let { taskList -> + + if (taskList.isEmpty()) { + return StatsResult( + activeTasksPercent = 0f, + completedTasksPercent = 0f + ) + } + + val totalTasks = taskList.size + val numberOfActiveTasks = taskList.count { it.isActive } + StatsResult( + activeTasksPercent = 100f * numberOfActiveTasks / tasks.size, + completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size + ) + } ?: StatsResult( + activeTasksPercent = 0f, + completedTasksPercent = 0f ) } diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt new file mode 100644 index 000000000..3e552c3f1 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt @@ -0,0 +1,88 @@ +package com.example.android.architecture.blueprints.todoapp.statistics + +import com.example.android.architecture.blueprints.todoapp.data.Task +import com.google.common.truth.Truth +import org.junit.Test + + +internal class StatisticsUtilsTest { + + + @Test + fun getActiveAndCompletedStats_noCompletedTasks_returnsHundredZero() { + + // Assign + val tasks = + listOf(Task(title = "First Task", description = "desc", isCompleted = false)) + + // Act + val statsResult = getActiveAndCompletedStats(tasks) + + // Assert + Truth.assertThat(statsResult.completedTasksPercent).isZero() + Truth.assertThat(statsResult.activeTasksPercent).isEqualTo(100f) + } + + @Test + fun getActiveAndCompletedStats_noActiveTasks_returnCompletedHundredActiveZero() { + + // Assign + val tasks = + listOf(Task(title = "First Task", description = "desc", isCompleted = true)) + + // Act + val statsResult = getActiveAndCompletedStats(tasks) + + // Assert + Truth.assertThat(statsResult.completedTasksPercent).isEqualTo(100f) + Truth.assertThat(statsResult.activeTasksPercent).isZero() + } + + @Test + fun getActiveAndCompletedStats_twoActiveThreeCompleted_returnFourtySixty() { + + // Assign + val tasks = listOf( + Task(title = "First Task", description = "desc", isCompleted = true), + Task(title = "First Task", description = "desc", isCompleted = true), + Task(title = "First Task", description = "desc", isCompleted = false), + Task(title = "First Task", description = "desc", isCompleted = false), + Task(title = "First Task", description = "desc", isCompleted = false) + ) + + // Act + val statsResult = getActiveAndCompletedStats(tasks) + + // Assert + Truth.assertThat(statsResult.completedTasksPercent).isEqualTo(40f) + Truth.assertThat(statsResult.activeTasksPercent).isEqualTo(60f) + } + + @Test + fun getActiveAndCompletedStats_emptyList_returnZero() { + + // Assign + val tasks = emptyList() + + // Act + val statsResult = getActiveAndCompletedStats(tasks) + + // Assert + Truth.assertThat(statsResult.completedTasksPercent).isZero() + Truth.assertThat(statsResult.activeTasksPercent).isZero() + } + + @Test + fun getActiveAndCompletedStats_nullList_returnZero() { + + // Assign + val tasks = null + + // Act + val statsResult = getActiveAndCompletedStats(tasks) + + // Assert + Truth.assertThat(statsResult.completedTasksPercent).isZero() + Truth.assertThat(statsResult.activeTasksPercent).isZero() + } +} \ No newline at end of file From de1a52a5addeb6ab9284056e7ef9c5abb394dfb0 Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Fri, 26 May 2023 15:38:02 +0530 Subject: [PATCH 02/15] Added Roboelectric and AndroidX Test suit to test android framework depdencies wthin local test in simulated environment --- app/build.gradle | 9 +++++++ .../todoapp/statistics/StatisticsUtilsTest.kt | 3 +++ .../todoapp/tasks/TasksViewModelTest.kt | 25 +++++++++++++++++++ build.gradle | 2 ++ 4 files changed, 39 insertions(+) create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt diff --git a/app/build.gradle b/app/build.gradle index c95292970..201c9b0e4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,10 @@ apply plugin: "androidx.navigation.safeargs.kotlin" android { compileSdkVersion rootProject.compileSdkVersion + testOptions.unitTests { + includeAndroidResources = true + } + defaultConfig { applicationId "com.example.android.architecture.blueprints.reactive" minSdkVersion rootProject.minSdkVersion @@ -54,6 +58,11 @@ dependencies { testImplementation "junit:junit:$junitVersion" testImplementation "com.google.truth:truth:1.1.2" + // AndroidX Test - JVM testing + testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion" + testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" + // AndroidX Test - Instrumented testing androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt index 3e552c3f1..cdac850dd 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt @@ -3,8 +3,11 @@ package com.example.android.architecture.blueprints.todoapp.statistics import com.example.android.architecture.blueprints.todoapp.data.Task import com.google.common.truth.Truth import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +@RunWith(JUnit4::class) internal class StatisticsUtilsTest { diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt new file mode 100644 index 000000000..251825d6d --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt @@ -0,0 +1,25 @@ +package com.example.android.architecture.blueprints.todoapp.tasks + +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + + +@RunWith(AndroidJUnit4::class) +internal class TasksViewModelTest { + + @Test + fun addNewTask_setNewTask() { + + // Assign a ViewModel + val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) + + // Assert adding a new Task + tasksViewModel.addNewTask() + + // Act new Task is added + // TODO: Test live data. + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index c851a4380..a1697467b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ buildscript { google() mavenCentral() } + dependencies { classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" @@ -46,6 +47,7 @@ ext { recyclerViewVersion = '1.2.1' roomVersion = '2.3.0' rulesVersion = '1.0.1' + robolectricVersion = '4.10.3' swipeRefreshLayoutVersion = '1.1.0' timberVersion = '4.7.1' } From 1d5be9c3602309d3934789afdfaa0cd1d39df59c Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Fri, 26 May 2023 15:38:02 +0530 Subject: [PATCH 03/15] Added Roboelectric and AndroidX Test suit to test android framework depdencies wthin local test in simulated environment --- app/build.gradle | 13 ++++++++++ .../todoapp/statistics/StatisticsUtilsTest.kt | 3 +++ .../todoapp/tasks/TasksViewModelTest.kt | 24 +++++++++++++++++++ build.gradle | 2 ++ 4 files changed, 42 insertions(+) create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt diff --git a/app/build.gradle b/app/build.gradle index c95292970..887e015f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,14 @@ apply plugin: "androidx.navigation.safeargs.kotlin" android { compileSdkVersion rootProject.compileSdkVersion + /** + * Allows to access Android Resources in your unit tests, including AndroidManifest file. + * This will resolve "no such as manifest file: " warning. + */ + testOptions.unitTests { + includeAndroidResources = true + } + defaultConfig { applicationId "com.example.android.architecture.blueprints.reactive" minSdkVersion rootProject.minSdkVersion @@ -54,6 +62,11 @@ dependencies { testImplementation "junit:junit:$junitVersion" testImplementation "com.google.truth:truth:1.1.2" + // AndroidX Test - JVM testing + testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion" + testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" + // AndroidX Test - Instrumented testing androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt index 3e552c3f1..cdac850dd 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt @@ -3,8 +3,11 @@ package com.example.android.architecture.blueprints.todoapp.statistics import com.example.android.architecture.blueprints.todoapp.data.Task import com.google.common.truth.Truth import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +@RunWith(JUnit4::class) internal class StatisticsUtilsTest { diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt new file mode 100644 index 000000000..421013663 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt @@ -0,0 +1,24 @@ +package com.example.android.architecture.blueprints.todoapp.tasks + +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +internal class TasksViewModelTest { + + @Test + fun addNewTask_setNewTask() { + + // Assign a ViewModel + val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) + + // Assert adding a new Task + tasksViewModel.addNewTask() + + // Act new Task is added + // TODO: Test live data. + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index c851a4380..a1697467b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ buildscript { google() mavenCentral() } + dependencies { classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" @@ -46,6 +47,7 @@ ext { recyclerViewVersion = '1.2.1' roomVersion = '2.3.0' rulesVersion = '1.0.1' + robolectricVersion = '4.10.3' swipeRefreshLayoutVersion = '1.1.0' timberVersion = '4.7.1' } From b2a035ec61a8d47d7bbe59c7efa035adf66a604a Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Fri, 26 May 2023 23:56:05 +0530 Subject: [PATCH 04/15] Test for Live Data --- app/build.gradle | 1 + .../todoapp/tasks/TasksViewModelTest.kt | 25 ++++++++++++++++--- build.gradle | 1 + 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 887e015f3..e308cf546 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,6 +66,7 @@ dependencies { testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion" testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" + testImplementation "androidx.arch.core:core-testing:$archTestingVersion" // AndroidX Test - Instrumented testing androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion" diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt index 421013663..7dcdc73ca 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt @@ -1,7 +1,12 @@ package com.example.android.architecture.blueprints.todoapp.tasks +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.example.android.architecture.blueprints.todoapp.Event +import com.google.common.truth.Truth +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -9,16 +14,28 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) internal class TasksViewModelTest { + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + @Test fun addNewTask_setNewTask() { // Assign a ViewModel val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) + val observer = Observer>{} + + try { + + // Act new Task is added + tasksViewModel.newTaskEvent.observeForever(observer) + tasksViewModel.addNewTask() - // Assert adding a new Task - tasksViewModel.addNewTask() + // Assert adding a new Task + val value = tasksViewModel.newTaskEvent.value + Truth.assertThat(value?.getContentIfNotHandled()).isNotNull() - // Act new Task is added - // TODO: Test live data. + } finally { + tasksViewModel.newTaskEvent.removeObserver(observer) + } } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index a1697467b..5cd054f34 100644 --- a/build.gradle +++ b/build.gradle @@ -50,4 +50,5 @@ ext { robolectricVersion = '4.10.3' swipeRefreshLayoutVersion = '1.1.0' timberVersion = '4.7.1' + archTestingVersion = '2.1.0' } From 062587a807f44569d05d76ed37ea3905f35e2cf7 Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Mon, 29 May 2023 12:14:48 +0530 Subject: [PATCH 05/15] Test added for addNewTask --- .../todoapp/tasks/LiveDataTestExtension.kt | 35 +++++++++++++++++ .../todoapp/tasks/TasksViewModelTest.kt | 39 +++++++++++++------ 2 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/LiveDataTestExtension.kt diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/LiveDataTestExtension.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/LiveDataTestExtension.kt new file mode 100644 index 000000000..5d591993a --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/LiveDataTestExtension.kt @@ -0,0 +1,35 @@ +package com.example.android.architecture.blueprints.todoapp.tasks + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun LiveData.getOrWaitValue(time: Long = 2, timeUnit: TimeUnit = TimeUnit.SECONDS): T { + + var data: T? = null + val latch = CountDownLatch(/* count= */ 1) + + val observer = object : Observer { + + override fun onChanged(t: T) { + data = t + latch.countDown() + removeObserver(this) + } + } + + observeForever(observer) + + try { + + if (!latch.await(time, timeUnit)) { + throw TimeoutException("Latch Timeout occurred") + } + } finally { + removeObserver(observer) + } + + return data as T +} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt index 7dcdc73ca..075784b2b 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt @@ -6,6 +6,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.example.android.architecture.blueprints.todoapp.Event import com.google.common.truth.Truth +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -17,25 +18,39 @@ internal class TasksViewModelTest { @get:Rule val instantExecutorRule = InstantTaskExecutorRule() + lateinit var tasksViewModel: TasksViewModel + + @Before + fun setupBefore() { + + tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) + } + @Test fun addNewTask_setNewTask() { // Assign a ViewModel - val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) - val observer = Observer>{} + // Done in setupBefore - try { + // Act new Task is added + tasksViewModel.addNewTask() + + // Assert adding a new Task + val value = tasksViewModel.newTaskEvent.getOrWaitValue() + Truth.assertThat(value.getContentIfNotHandled()).isNotNull() + } + + @Test + fun setFiltering_toAllTask_taskAddNewVisible() { - // Act new Task is added - tasksViewModel.newTaskEvent.observeForever(observer) - tasksViewModel.addNewTask() + // Assign + // Done in setupBefore - // Assert adding a new Task - val value = tasksViewModel.newTaskEvent.value - Truth.assertThat(value?.getContentIfNotHandled()).isNotNull() + // Act + tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS) - } finally { - tasksViewModel.newTaskEvent.removeObserver(observer) - } + // Assert + val value = tasksViewModel.tasksAddViewVisible.getOrWaitValue() + Truth.assertThat(value).isTrue() } } \ No newline at end of file From 548e4f6b0d1a3787358d1e5cd3060bff1ef74a7f Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Mon, 29 May 2023 17:38:34 +0530 Subject: [PATCH 06/15] DefaultRepository Test --- app/build.gradle | 1 + .../data/source/DefaultTasksRepository.kt | 32 ++++----- .../data/source/DefaultTasksRepositoryTest.kt | 54 +++++++++++++++ .../todoapp/data/source/FakeDataSource.kt | 65 +++++++++++++++++++ 4 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDataSource.kt diff --git a/app/build.gradle b/app/build.gradle index e308cf546..332dc90f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,6 +67,7 @@ dependencies { testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" testImplementation "androidx.arch.core:core-testing:$archTestingVersion" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" // AndroidX Test - Instrumented testing androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion" diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt index a4123e54c..30f5fc4bd 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt @@ -24,20 +24,16 @@ import com.example.android.architecture.blueprints.todoapp.data.Task 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.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* /** * Concrete implementation to load tasks from the data sources into a cache. */ -class DefaultTasksRepository private constructor(application: Application) { - - private val tasksRemoteDataSource: TasksDataSource - private val tasksLocalDataSource: TasksDataSource +class DefaultTasksRepository( + private val tasksRemoteDataSource: TasksDataSource, + private val tasksLocalDataSource: TasksDataSource, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) { companion object { @Volatile @@ -45,22 +41,18 @@ class DefaultTasksRepository private constructor(application: Application) { fun getRepository(app: Application): DefaultTasksRepository { return INSTANCE ?: synchronized(this) { - DefaultTasksRepository(app).also { + val database = + Room.databaseBuilder(app, ToDoDatabase::class.java, "Tasks.db").build() + DefaultTasksRepository( + TasksRemoteDataSource, + TasksLocalDataSource(database.taskDao()) + ).also { INSTANCE = it } } } } - init { - val database = Room.databaseBuilder(application.applicationContext, - ToDoDatabase::class.java, "Tasks.db") - .build() - - tasksRemoteDataSource = TasksRemoteDataSource - tasksLocalDataSource = TasksLocalDataSource(database.taskDao()) - } - suspend fun getTasks(forceUpdate: Boolean = false): Result> { if (forceUpdate) { try { @@ -113,7 +105,7 @@ class DefaultTasksRepository private constructor(application: Application) { /** * Relies on [getTasks] to fetch data and picks the task with the same ID. */ - suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result { + suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result { if (forceUpdate) { updateTaskFromRemoteDataSource(taskId) } diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt new file mode 100644 index 000000000..1b5ee658a --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt @@ -0,0 +1,54 @@ +package com.example.android.architecture.blueprints.todoapp.data.source + +import com.example.android.architecture.blueprints.todoapp.data.Result +import com.example.android.architecture.blueprints.todoapp.data.Task +import com.google.common.truth.Truth +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +internal class DefaultTasksRepositoryTest { + + private val task1 = Task("Title1","Description1") + private val task2 = Task("Title2","Description2") + private val task3 = Task("Title3","Description3") + private val remoteTask = listOf(task1, task2).sortedBy { it.id } + private val localTask = listOf(task3).sortedBy { it.id } + private val newTask = listOf(task3).sortedBy { it.id } + + private lateinit var tasksRemoteDataSource: FakeDataSource + private lateinit var tasksLocalDataSource: FakeDataSource + private lateinit var defaultDataSource: DefaultTasksRepository + + @Before + fun createRepository() { + + tasksRemoteDataSource = FakeDataSource(remoteTask.toMutableList()) + tasksLocalDataSource = FakeDataSource(remoteTask.toMutableList()) + defaultDataSource = DefaultTasksRepository(tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined) + } + + @Test + fun getTasks_requestAllTaskFromRemoteDataSource_returnsRemoteTask() = runBlockingTest { + // Assign + val forceUpdate = true + + // Act + val tasks = defaultDataSource.getTasks(forceUpdate) as Result.Success + + // Assert + Truth.assertThat(tasks.data).isEqualTo(remoteTask) + + } + + @Test + fun saveTask() { + } + + @Test + fun deleteAllTasks() { + } +} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDataSource.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDataSource.kt new file mode 100644 index 000000000..795ff39de --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDataSource.kt @@ -0,0 +1,65 @@ +package com.example.android.architecture.blueprints.todoapp.data.source + +import androidx.lifecycle.LiveData +import com.example.android.architecture.blueprints.todoapp.data.Result +import com.example.android.architecture.blueprints.todoapp.data.Task + +class FakeDataSource(private var tasks: MutableList? = mutableListOf()) : TasksDataSource { + override fun observeTasks(): LiveData>> { + TODO("Not yet implemented") + } + + override suspend fun getTasks(): Result> { + return tasks?.let { + Result.Success(ArrayList(it)) + } ?: Result.Error(IllegalStateException("Task is empty")) + } + + override suspend fun refreshTasks() { + TODO("Not yet implemented") + } + + override fun observeTask(taskId: String): LiveData> { + TODO("Not yet implemented") + } + + override suspend fun getTask(taskId: String): Result { + TODO("Not yet implemented") + } + + override suspend fun refreshTask(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun saveTask(task: Task) { + tasks?.add(task) + } + + override suspend fun completeTask(task: Task) { + TODO("Not yet implemented") + } + + override suspend fun completeTask(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun activateTask(task: Task) { + TODO("Not yet implemented") + } + + override suspend fun activateTask(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun clearCompletedTasks() { + TODO("Not yet implemented") + } + + override suspend fun deleteAllTasks() { + tasks?.clear() + } + + override suspend fun deleteTask(taskId: String) { + TODO("Not yet implemented") + } +} \ No newline at end of file From 7399f8aecdd1abfd95eac8a27a6cc2dd8e2281e3 Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Tue, 30 May 2023 12:41:46 +0530 Subject: [PATCH 07/15] Add fake Task Repository --- .../data/source/DefaultTasksRepository.kt | 34 ++++---- .../data/source/IDefaultTasksRepository.kt | 40 +++++++++ .../data/source/FakeDefaultTasksRepository.kt | 87 +++++++++++++++++++ 3 files changed, 144 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/IDefaultTasksRepository.kt create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt index 30f5fc4bd..37303a43a 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt @@ -33,7 +33,7 @@ class DefaultTasksRepository( private val tasksRemoteDataSource: TasksDataSource, private val tasksLocalDataSource: TasksDataSource, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO -) { +) : IDefaultTasksRepository { companion object { @Volatile @@ -53,7 +53,7 @@ class DefaultTasksRepository( } } - suspend fun getTasks(forceUpdate: Boolean = false): Result> { + override suspend fun getTasks(forceUpdate: Boolean): Result> { if (forceUpdate) { try { updateTasksFromRemoteDataSource() @@ -64,19 +64,19 @@ class DefaultTasksRepository( return tasksLocalDataSource.getTasks() } - suspend fun refreshTasks() { + override suspend fun refreshTasks() { updateTasksFromRemoteDataSource() } - fun observeTasks(): LiveData>> { + override fun observeTasks(): LiveData>> { return tasksLocalDataSource.observeTasks() } - suspend fun refreshTask(taskId: String) { + override suspend fun refreshTask(taskId: String) { updateTaskFromRemoteDataSource(taskId) } - private suspend fun updateTasksFromRemoteDataSource() { + override suspend fun updateTasksFromRemoteDataSource() { val remoteTasks = tasksRemoteDataSource.getTasks() if (remoteTasks is Success) { @@ -90,11 +90,11 @@ class DefaultTasksRepository( } } - fun observeTask(taskId: String): LiveData> { + override fun observeTask(taskId: String): LiveData> { return tasksLocalDataSource.observeTask(taskId) } - private suspend fun updateTaskFromRemoteDataSource(taskId: String) { + override suspend fun updateTaskFromRemoteDataSource(taskId: String) { val remoteTask = tasksRemoteDataSource.getTask(taskId) if (remoteTask is Success) { @@ -105,7 +105,7 @@ class DefaultTasksRepository( /** * Relies on [getTasks] to fetch data and picks the task with the same ID. */ - suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result { + override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result { if (forceUpdate) { updateTaskFromRemoteDataSource(taskId) } @@ -119,14 +119,14 @@ class DefaultTasksRepository( } } - suspend fun completeTask(task: Task) { + override suspend fun completeTask(task: Task) { coroutineScope { launch { tasksRemoteDataSource.completeTask(task) } launch { tasksLocalDataSource.completeTask(task) } } } - suspend fun completeTask(taskId: String) { + override suspend fun completeTask(taskId: String) { withContext(ioDispatcher) { (getTaskWithId(taskId) as? Success)?.let { it -> completeTask(it.data) @@ -134,14 +134,14 @@ class DefaultTasksRepository( } } - suspend fun activateTask(task: Task) = withContext(ioDispatcher) { + override suspend fun activateTask(task: Task) = withContext(ioDispatcher) { coroutineScope { launch { tasksRemoteDataSource.activateTask(task) } launch { tasksLocalDataSource.activateTask(task) } } } - suspend fun activateTask(taskId: String) { + override suspend fun activateTask(taskId: String) { withContext(ioDispatcher) { (getTaskWithId(taskId) as? Success)?.let { it -> activateTask(it.data) @@ -149,14 +149,14 @@ class DefaultTasksRepository( } } - suspend fun clearCompletedTasks() { + override suspend fun clearCompletedTasks() { coroutineScope { launch { tasksRemoteDataSource.clearCompletedTasks() } launch { tasksLocalDataSource.clearCompletedTasks() } } } - suspend fun deleteAllTasks() { + override suspend fun deleteAllTasks() { withContext(ioDispatcher) { coroutineScope { launch { tasksRemoteDataSource.deleteAllTasks() } @@ -165,14 +165,14 @@ class DefaultTasksRepository( } } - suspend fun deleteTask(taskId: String) { + override suspend fun deleteTask(taskId: String) { coroutineScope { launch { tasksRemoteDataSource.deleteTask(taskId) } launch { tasksLocalDataSource.deleteTask(taskId) } } } - private suspend fun getTaskWithId(id: String): Result { + override suspend fun getTaskWithId(id: String): Result { return tasksLocalDataSource.getTask(id) } } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/IDefaultTasksRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/IDefaultTasksRepository.kt new file mode 100644 index 000000000..976150774 --- /dev/null +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/IDefaultTasksRepository.kt @@ -0,0 +1,40 @@ +package com.example.android.architecture.blueprints.todoapp.data.source + +import androidx.lifecycle.LiveData +import com.example.android.architecture.blueprints.todoapp.data.Result +import com.example.android.architecture.blueprints.todoapp.data.Task + +interface IDefaultTasksRepository { + suspend fun getTasks(forceUpdate: Boolean = false): Result> + + suspend fun refreshTasks() + fun observeTasks(): LiveData>> + + suspend fun refreshTask(taskId: String) + + suspend fun updateTasksFromRemoteDataSource() + fun observeTask(taskId: String): LiveData> + + suspend fun updateTaskFromRemoteDataSource(taskId: String) + + /** + * Relies on [getTasks] to fetch data and picks the task with the same ID. + */ + suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result + + suspend fun completeTask(task: Task) + + suspend fun completeTask(taskId: String) + + suspend fun activateTask(task: Task) + + suspend fun activateTask(taskId: String) + + suspend fun clearCompletedTasks() + + suspend fun deleteAllTasks() + + suspend fun deleteTask(taskId: String) + + suspend fun getTaskWithId(id: String): Result +} \ 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 new file mode 100644 index 000000000..df2bc4dbc --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDefaultTasksRepository.kt @@ -0,0 +1,87 @@ +package com.example.android.architecture.blueprints.todoapp.data.source + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.example.android.architecture.blueprints.todoapp.data.Result +import com.example.android.architecture.blueprints.todoapp.data.Task +import kotlinx.coroutines.runBlocking + +class FakeDefaultTasksRepository : IDefaultTasksRepository { + + private val tasksData = LinkedHashMap() + private val observableTask = MutableLiveData>>() + + override suspend fun getTasks(forceUpdate: Boolean): Result> { + return Result.Success(tasksData.values.toList()) + } + + override suspend fun refreshTasks() { + observableTask.value = getTasks() + } + + override fun observeTasks(): LiveData>> { + runBlocking { refreshTasks() } + return observableTask + } + + override suspend fun refreshTask(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun updateTasksFromRemoteDataSource() { + TODO("Not yet implemented") + } + + override fun observeTask(taskId: String): LiveData> { + TODO("Not yet implemented") + } + + override suspend fun updateTaskFromRemoteDataSource(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result { + TODO("Not yet implemented") + } + + override suspend fun completeTask(task: Task) { + val taskCompleted = task.copy(isCompleted = true) + tasksData[task.id] = taskCompleted + refreshTasks() + } + + override suspend fun completeTask(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun activateTask(task: Task) { + TODO("Not yet implemented") + } + + override suspend fun activateTask(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun clearCompletedTasks() { + TODO("Not yet implemented") + } + + override suspend fun deleteAllTasks() { + TODO("Not yet implemented") + } + + override suspend fun deleteTask(taskId: String) { + TODO("Not yet implemented") + } + + override suspend fun getTaskWithId(id: String): Result { + TODO("Not yet implemented") + } + + fun addTask(vararg tasks: Task) { + tasks.forEach { task -> + tasksData[task.id] = task + } + runBlocking { refreshTasks() } + } +} \ No newline at end of file From 33980cac271930115e6a1d23c0fe9347fb7c2cc0 Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Tue, 30 May 2023 15:38:01 +0530 Subject: [PATCH 08/15] Dependency Injection into ViewModel using ViewModelFactory --- .../data/source/DefaultTasksRepository.kt | 17 ++++++++++------- .../todoapp/taskdetail/TaskDetailFragment.kt | 14 +++++++------- .../todoapp/taskdetail/TaskDetailViewModel.kt | 14 ++++++++++---- .../blueprints/todoapp/tasks/TasksFragment.kt | 15 +++++++-------- .../blueprints/todoapp/tasks/TasksViewModel.kt | 11 +++++++++-- .../todoapp/tasks/TasksViewModelTest.kt | 15 ++++++++++++--- 6 files changed, 55 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt index 37303a43a..f61df2cef 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt @@ -41,13 +41,16 @@ class DefaultTasksRepository( fun getRepository(app: Application): DefaultTasksRepository { return INSTANCE ?: synchronized(this) { - val database = - Room.databaseBuilder(app, ToDoDatabase::class.java, "Tasks.db").build() - DefaultTasksRepository( - TasksRemoteDataSource, - TasksLocalDataSource(database.taskDao()) - ).also { - INSTANCE = it + // Double check for thread race condition + INSTANCE ?: kotlin.run { + val database = + Room.databaseBuilder(app, ToDoDatabase::class.java, "Tasks.db").build() + DefaultTasksRepository( + TasksRemoteDataSource, + TasksLocalDataSource(database.taskDao()) + ).also { + INSTANCE = it + } } } } 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 399436588..5dedc3e42 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 @@ -16,18 +16,14 @@ package com.example.android.architecture.blueprints.todoapp.taskdetail import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.example.android.architecture.blueprints.todoapp.EventObserver import com.example.android.architecture.blueprints.todoapp.R +import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository 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 @@ -42,7 +38,11 @@ class TaskDetailFragment : Fragment() { private val args: TaskDetailFragmentArgs by navArgs() - private val viewModel by viewModels() + private val viewModel by viewModels() { + TaskDetailViewModelFactory( + DefaultTasksRepository.getRepository(requireActivity().application) + ) + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { setupFab() diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt index 4b9997e58..ce3a80aed 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt @@ -15,7 +15,6 @@ */ package com.example.android.architecture.blueprints.todoapp.taskdetail -import android.app.Application import androidx.annotation.StringRes import androidx.lifecycle.* import com.example.android.architecture.blueprints.todoapp.Event @@ -23,17 +22,16 @@ import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.data.Result import com.example.android.architecture.blueprints.todoapp.data.Result.Success import com.example.android.architecture.blueprints.todoapp.data.Task -import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository +import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository import kotlinx.coroutines.launch /** * ViewModel for the Details screen. */ -class TaskDetailViewModel(application: Application) : AndroidViewModel(application) { +class TaskDetailViewModel(private val tasksRepository: IDefaultTasksRepository) : ViewModel() { // Note, for testing and architecture purposes, it's bad practice to construct the repository // here. We'll show you how to fix this during the codelab - private val tasksRepository = DefaultTasksRepository.getRepository(application) private val _taskId = MutableLiveData() @@ -117,3 +115,11 @@ class TaskDetailViewModel(application: Application) : AndroidViewModel(applicati _snackbarText.value = Event(message) } } + +@Suppress("UNCHECKED_CAST") +class TaskDetailViewModelFactory(private val tasksRepository: IDefaultTasksRepository) : + ViewModelProvider.NewInstanceFactory() { + + override fun create(modelClass: Class): T = + TaskDetailViewModel(tasksRepository) as T +} diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt index 0fd7e724e..f5ff594b0 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt @@ -17,12 +17,7 @@ package com.example.android.architecture.blueprints.todoapp.tasks import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -31,10 +26,10 @@ import androidx.navigation.fragment.navArgs import com.example.android.architecture.blueprints.todoapp.EventObserver import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.data.Task +import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository import com.example.android.architecture.blueprints.todoapp.databinding.TasksFragBinding import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout import com.example.android.architecture.blueprints.todoapp.util.setupSnackbar -import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import timber.log.Timber @@ -43,7 +38,11 @@ import timber.log.Timber */ class TasksFragment : Fragment() { - private val viewModel by viewModels() + private val viewModel by viewModels() { + TaskViewModelFactory( + DefaultTasksRepository.getRepository(requireActivity().application) + ) + } private val args: TasksFragmentArgs by navArgs() diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt index 8cc67bb53..52f3d9d7b 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt @@ -25,17 +25,17 @@ import com.example.android.architecture.blueprints.todoapp.data.Result import com.example.android.architecture.blueprints.todoapp.data.Result.Success import com.example.android.architecture.blueprints.todoapp.data.Task 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.data.source.TasksDataSource import kotlinx.coroutines.launch /** * ViewModel for the task list screen. */ -class TasksViewModel(application: Application) : AndroidViewModel(application) { +class TasksViewModel(private val tasksRepository: IDefaultTasksRepository) : ViewModel() { // Note, for testing and architecture purposes, it's bad practice to construct the repository // here. We'll show you how to fix this during the codelab - private val tasksRepository = DefaultTasksRepository.getRepository(application) private val _forceUpdate = MutableLiveData(false) @@ -231,3 +231,10 @@ class TasksViewModel(application: Application) : AndroidViewModel(application) { _forceUpdate.value = true } } + +@Suppress("UNCHECKED_CAST") +class TaskViewModelFactory(private val taskRepository: IDefaultTasksRepository) + : ViewModelProvider.NewInstanceFactory() { + + override fun create(modelClass: Class): T = TasksViewModel(taskRepository) as T +} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt index 075784b2b..540fe7177 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt @@ -5,25 +5,34 @@ import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.example.android.architecture.blueprints.todoapp.Event +import com.example.android.architecture.blueprints.todoapp.data.Task +import com.example.android.architecture.blueprints.todoapp.data.source.FakeDefaultTasksRepository +import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository import com.google.common.truth.Truth import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) + internal class TasksViewModelTest { @get:Rule val instantExecutorRule = InstantTaskExecutorRule() + lateinit var tasksRepository: FakeDefaultTasksRepository lateinit var tasksViewModel: TasksViewModel @Before fun setupBefore() { - tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) + val task1 = Task("Title1", "Description1") + val task2 = Task("Title2", "Description2", true) + val task3 = Task("Title3", "Description3", true) + tasksRepository = FakeDefaultTasksRepository() + tasksRepository.addTask(task1, task2, task3) + + tasksViewModel = TasksViewModel(tasksRepository) } @Test From 1a60143caa4f322094c2994ec3ca436814c73a6c Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Tue, 30 May 2023 18:01:19 +0530 Subject: [PATCH 09/15] Depdency added for Instrumented Changes --- app/build.gradle | 15 +++++++++-- .../taskdetail/TaskDetailFragmentTest.kt | 27 +++++++++++++++++++ build.gradle | 1 + 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragmentTest.kt diff --git a/app/build.gradle b/app/build.gradle index 332dc90f4..2b2a410c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,6 +33,10 @@ android { enabled = true enabledForTests = true } + + packagingOptions { + resources.excludes.add("META-INF/*") + } } dependencies { @@ -70,8 +74,15 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" // AndroidX Test - Instrumented testing - androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion" - androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" + debugImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion" + debugImplementation "androidx.test.espresso:espresso-core:$espressoVersion" + + // Dependencies for Android instrumented unit tests + debugImplementation "junit:junit:$junitVersion" + debugImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" + + debugImplementation "androidx.fragment:fragment-testing:$fragmentVersion" + debugImplementation "androidx.test:core:$androidXTestCoreVersion" // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 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 new file mode 100644 index 000000000..2ae36db8a --- /dev/null +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragmentTest.kt @@ -0,0 +1,27 @@ +package com.example.android.architecture.blueprints.todoapp.taskdetail + +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.data.Task +import org.junit.Test +import org.junit.runner.RunWith + +@MediumTest +@RunWith(AndroidJUnit4::class) +internal class TaskDetailFragmentTest { + + @Test + fun activeTaskDetails_DisplayedInUi() { + // Assign - Add active (incomplete) task to the DB + val activeTask = Task("Active Task", "AndroidX Rocks", false) + + // Act - Details fragment launched to display task + val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle() + launchFragmentInContainer(bundle, R.style.AppTheme) + + // Assert + val value = true + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5cd054f34..db9c94d56 100644 --- a/build.gradle +++ b/build.gradle @@ -51,4 +51,5 @@ ext { swipeRefreshLayoutVersion = '1.1.0' timberVersion = '4.7.1' archTestingVersion = '2.1.0' + fragmentVersion = '1.1.0-alpha07' } From ff14286c6ec5e83fad8ddb7921cbfc2e4ba02df9 Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Wed, 31 May 2023 18:35:37 +0530 Subject: [PATCH 10/15] Create service locator to inject into fragments --- .../todoapp/taskdetail/TaskDetailFragmentTest.kt | 1 + .../blueprints/todoapp/ServiceLocator.kt | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 app/src/main/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt 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 2ae36db8a..58f2018fd 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 @@ -21,6 +21,7 @@ internal class TaskDetailFragmentTest { val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle() launchFragmentInContainer(bundle, R.style.AppTheme) + Thread.sleep(5000) // Assert val value = true } 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 new file mode 100644 index 000000000..387001c2b --- /dev/null +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt @@ -0,0 +1,12 @@ +package com.example.android.architecture.blueprints.todoapp + +import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository +import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase + +object ServiceLocator { + + private var database: ToDoDatabase? = null + + @Volatile + private var tasksRepository: IDefaultTasksRepository? = null +} \ No newline at end of file From 206d4cb18c00f987c05ddcc30c9f12e0fe600083 Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Wed, 31 May 2023 18:35:37 +0530 Subject: [PATCH 11/15] Create service locator to inject into fragments --- .../blueprints/todoapp/ServiceLocator.kt | 35 +++++++++++++++++++ .../blueprints/todoapp/TodoApplication.kt | 4 +++ .../addedittask/AddEditTaskViewModel.kt | 3 +- .../data/source/DefaultTasksRepository.kt | 23 +----------- .../data/source/IDefaultTasksRepository.kt | 2 ++ .../todoapp/statistics/StatisticsViewModel.kt | 3 +- .../todoapp/taskdetail/TaskDetailFragment.kt | 3 +- .../blueprints/todoapp/tasks/TasksFragment.kt | 3 +- 8 files changed, 50 insertions(+), 26 deletions(-) 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 387001c2b..7de2c64e4 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,7 +1,13 @@ package com.example.android.architecture.blueprints.todoapp +import android.content.Context +import androidx.room.Room +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.data.source.TasksDataSource +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 object ServiceLocator { @@ -9,4 +15,33 @@ object ServiceLocator { @Volatile private var tasksRepository: IDefaultTasksRepository? = null + + fun provideTaskRepository(context: Context): IDefaultTasksRepository { + + synchronized(this) { + return tasksRepository ?: createTaskRepository(context) + } + } + + private fun createTaskRepository(context: Context): IDefaultTasksRepository { + + val repository = + DefaultTasksRepository(TasksRemoteDataSource, createLocalDataSource(context)) + tasksRepository = repository + return repository + } + + private fun createLocalDataSource(context: Context): TasksDataSource { + val database = database ?: createDatabase(context) + return TasksLocalDataSource(database.taskDao()) + } + + private fun createDatabase(context: Context): ToDoDatabase { + val result = Room.databaseBuilder( + context.applicationContext, + ToDoDatabase::class.java, /* name= */"Tasks.db" + ).build() + database = result + return result + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt index 6ae3f457e..109e06eae 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt @@ -17,6 +17,7 @@ package com.example.android.architecture.blueprints.todoapp import android.app.Application +import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository import timber.log.Timber import timber.log.Timber.DebugTree @@ -28,6 +29,9 @@ import timber.log.Timber.DebugTree */ class TodoApplication : Application() { + val tasksRepository: IDefaultTasksRepository + = ServiceLocator.provideTaskRepository(applicationContext) + override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) Timber.plant(DebugTree()) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt index 77338668a..80b240906 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt @@ -20,6 +20,7 @@ import android.app.Application import androidx.lifecycle.* import com.example.android.architecture.blueprints.todoapp.Event import com.example.android.architecture.blueprints.todoapp.R +import com.example.android.architecture.blueprints.todoapp.TodoApplication import com.example.android.architecture.blueprints.todoapp.data.Result.Success import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository @@ -32,7 +33,7 @@ class AddEditTaskViewModel(application: Application) : AndroidViewModel(applicat // Note, for testing and architecture purposes, it's bad practice to construct the repository // here. We'll show you how to fix this during the codelab - private val tasksRepository = DefaultTasksRepository.getRepository(application) + private val tasksRepository = (application as TodoApplication).tasksRepository // Two-way databinding, exposing MutableLiveData val title = MutableLiveData() diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt index f61df2cef..02ae63f91 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt @@ -35,27 +35,6 @@ class DefaultTasksRepository( private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) : IDefaultTasksRepository { - companion object { - @Volatile - private var INSTANCE: DefaultTasksRepository? = null - - fun getRepository(app: Application): DefaultTasksRepository { - return INSTANCE ?: synchronized(this) { - // Double check for thread race condition - INSTANCE ?: kotlin.run { - val database = - Room.databaseBuilder(app, ToDoDatabase::class.java, "Tasks.db").build() - DefaultTasksRepository( - TasksRemoteDataSource, - TasksLocalDataSource(database.taskDao()) - ).also { - INSTANCE = it - } - } - } - } - } - override suspend fun getTasks(forceUpdate: Boolean): Result> { if (forceUpdate) { try { @@ -115,7 +94,7 @@ class DefaultTasksRepository( return tasksLocalDataSource.getTask(taskId) } - suspend fun saveTask(task: Task) { + override suspend fun saveTask(task: Task) { coroutineScope { launch { tasksRemoteDataSource.saveTask(task) } launch { tasksLocalDataSource.saveTask(task) } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/IDefaultTasksRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/IDefaultTasksRepository.kt index 976150774..d1cdccccf 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/IDefaultTasksRepository.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/IDefaultTasksRepository.kt @@ -37,4 +37,6 @@ interface IDefaultTasksRepository { suspend fun deleteTask(taskId: String) suspend fun getTaskWithId(id: String): Result + + suspend fun saveTask(task: Task) } \ No newline at end of file diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt index 3892b373f..2099d8056 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt @@ -18,6 +18,7 @@ package com.example.android.architecture.blueprints.todoapp.statistics import android.app.Application import androidx.lifecycle.* +import com.example.android.architecture.blueprints.todoapp.TodoApplication import com.example.android.architecture.blueprints.todoapp.data.Result import com.example.android.architecture.blueprints.todoapp.data.Result.Error import com.example.android.architecture.blueprints.todoapp.data.Result.Success @@ -32,7 +33,7 @@ class StatisticsViewModel(application: Application) : AndroidViewModel(applicati // Note, for testing and architecture purposes, it's bad practice to construct the repository // here. We'll show you how to fix this during the codelab - private val tasksRepository = DefaultTasksRepository.getRepository(application) + private val tasksRepository = (application as TodoApplication).tasksRepository private val tasks: LiveData>> = tasksRepository.observeTasks() private val _dataLoading = MutableLiveData(false) 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 5dedc3e42..0f69fa49b 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 @@ -23,6 +23,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs 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.databinding.TaskdetailFragBinding import com.example.android.architecture.blueprints.todoapp.tasks.DELETE_RESULT_OK @@ -40,7 +41,7 @@ class TaskDetailFragment : Fragment() { private val viewModel by viewModels() { TaskDetailViewModelFactory( - DefaultTasksRepository.getRepository(requireActivity().application) + (requireActivity().application as TodoApplication).tasksRepository ) } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt index f5ff594b0..c220a15aa 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt @@ -25,6 +25,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs 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.Task import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository import com.example.android.architecture.blueprints.todoapp.databinding.TasksFragBinding @@ -40,7 +41,7 @@ class TasksFragment : Fragment() { private val viewModel by viewModels() { TaskViewModelFactory( - DefaultTasksRepository.getRepository(requireActivity().application) + (requireActivity().application as TodoApplication).tasksRepository ) } From d7c91d77c0321425d230b605ba906a170105b73b Mon Sep 17 00:00:00 2001 From: Apple Date: Fri, 2 Jun 2023 21:56:02 +0530 Subject: [PATCH 12/15] 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 000000000..dea0fbd3b --- /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 df2bc4dbc..3972060c9 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 13/15] 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 000000000..dea0fbd3b --- /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 58f2018fd..d0ae58ede 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 7de2c64e4..11306190b 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 0f69fa49b..5c2e745dd 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 df2bc4dbc..3972060c9 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 d2bf4dc46eec1cd5612f97305a5f5d45b0e78b1e Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Wed, 31 May 2023 18:35:37 +0530 Subject: [PATCH 14/15] Create service locator to inject into fragments --- .../todoapp/taskdetail/TaskDetailFragmentTest.kt | 1 + .../blueprints/todoapp/ServiceLocator.kt | 12 ++++++++++++ .../data/source/FakeDefaultTasksRepository.kt | 2 +- .../blueprints/todoapp/tasks/TasksViewModelTest.kt | 10 ++-------- 4 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt 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 2ae36db8a..58f2018fd 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 @@ -21,6 +21,7 @@ internal class TaskDetailFragmentTest { val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle() launchFragmentInContainer(bundle, R.style.AppTheme) + Thread.sleep(5000) // Assert val value = true } 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 new file mode 100644 index 000000000..387001c2b --- /dev/null +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt @@ -0,0 +1,12 @@ +package com.example.android.architecture.blueprints.todoapp + +import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository +import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase + +object ServiceLocator { + + private var database: ToDoDatabase? = null + + @Volatile + private var tasksRepository: IDefaultTasksRepository? = null +} \ 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 df2bc4dbc..1d1204546 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,7 +78,7 @@ class FakeDefaultTasksRepository : IDefaultTasksRepository { TODO("Not yet implemented") } - fun addTask(vararg tasks: Task) { + fun addTasks(vararg tasks: Task) { tasks.forEach { task -> tasksData[task.id] = task } diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt index 540fe7177..b4d4a4988 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt @@ -1,20 +1,14 @@ package com.example.android.architecture.blueprints.todoapp.tasks import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.example.android.architecture.blueprints.todoapp.Event import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.FakeDefaultTasksRepository -import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository import com.google.common.truth.Truth import org.junit.Before import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith - + internal class TasksViewModelTest { @get:Rule @@ -30,7 +24,7 @@ internal class TasksViewModelTest { val task2 = Task("Title2", "Description2", true) val task3 = Task("Title3", "Description3", true) tasksRepository = FakeDefaultTasksRepository() - tasksRepository.addTask(task1, task2, task3) + tasksRepository.addTasks(task1, task2, task3) tasksViewModel = TasksViewModel(tasksRepository) } From e9a21bc9c4674b39251579d8d82fa547383d9a7d Mon Sep 17 00:00:00 2001 From: Pankaj Zanzane Date: Wed, 31 May 2023 18:35:37 +0530 Subject: [PATCH 15/15] Create service locator to inject into fragments --- .../todoapp/data/source/FakeDefaultTasksRepository.kt | 4 ++-- .../blueprints/todoapp/tasks/TasksViewModelTest.kt | 10 ++-------- 2 files changed, 4 insertions(+), 10 deletions(-) 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 3972060c9..82bbc3853 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 @@ -77,12 +77,12 @@ class FakeDefaultTasksRepository : IDefaultTasksRepository { override suspend fun getTaskWithId(id: String): Result { TODO("Not yet implemented") } - + override suspend fun saveTask(task: Task) { TODO("Not yet implemented") } - fun addTask(vararg tasks: Task) { + fun addTasks(vararg tasks: Task) { tasks.forEach { task -> tasksData[task.id] = task } diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt index 540fe7177..b4d4a4988 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt @@ -1,20 +1,14 @@ package com.example.android.architecture.blueprints.todoapp.tasks import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.example.android.architecture.blueprints.todoapp.Event import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.FakeDefaultTasksRepository -import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository import com.google.common.truth.Truth import org.junit.Before import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith - + internal class TasksViewModelTest { @get:Rule @@ -30,7 +24,7 @@ internal class TasksViewModelTest { val task2 = Task("Title2", "Description2", true) val task3 = Task("Title3", "Description3", true) tasksRepository = FakeDefaultTasksRepository() - tasksRepository.addTask(task1, task2, task3) + tasksRepository.addTasks(task1, task2, task3) tasksViewModel = TasksViewModel(tasksRepository) }