From 2b8542dc3d6bca41b5f043cc5233f006a8927678 Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Tue, 28 Feb 2023 15:12:48 -0500 Subject: [PATCH 01/10] Build Project for Education --- build.gradle | 8 ++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index c851a4380..ec29cbb07 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ buildscript { ext.kotlinVersion = '1.5.31' - ext.navigationVersion = '2.3.5' + ext.navigationVersion = '2.4.1' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion" @@ -27,7 +27,7 @@ ext { // Sdk and tools minSdkVersion = 21 targetSdkVersion = 31 - compileSdkVersion = 31 + compileSdkVersion = 33 // App dependencies androidXVersion = '1.0.0' @@ -44,7 +44,7 @@ ext { junitVersion = '4.13.2' materialVersion = '1.4.0' recyclerViewVersion = '1.2.1' - roomVersion = '2.3.0' + roomVersion = '2.5.0' rulesVersion = '1.0.1' swipeRefreshLayoutVersion = '1.1.0' timberVersion = '4.7.1' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e228a385f..b862899ea 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Jun 14 12:47:31 UTC 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From a471b35abac67f2903748af3af877a6df59d07f4 Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Wed, 1 Mar 2023 11:43:06 -0500 Subject: [PATCH 02/10] Testing getActiveAndCompletedStats --- app/build.gradle | 6 ++ .../todoapp/statistics/StatisticsUtils.kt | 5 ++ .../todoapp/statistics/StatisticsUtilsTest.kt | 70 +++++++++++++++++++ build.gradle | 1 + 4 files changed, 82 insertions(+) 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..9044e0dba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,9 @@ dependencies { // Architecture Components implementation "androidx.room:room-runtime:$roomVersion" + testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.12' kapt "androidx.room:room-compiler:$roomVersion" implementation "androidx.room:room-ktx:$roomVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion" @@ -60,4 +63,7 @@ dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion" + + // Other dependencies + testImplementation "org.hamcrest:hamcrest:$hamcrestVersion" } 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..32593dd12 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,8 +22,13 @@ 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 { + + if (tasks.isNullOrEmpty()){ + return StatsResult(0f,0f) + } val totalTasks = tasks!!.size val numberOfActiveTasks = tasks.count { it.isActive } + return StatsResult( activeTasksPercent = 100f * numberOfActiveTasks / tasks.size, completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size 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..d64210b74 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt @@ -0,0 +1,70 @@ +package com.example.android.architecture.blueprints.todoapp.statistics + +import com.example.android.architecture.blueprints.todoapp.data.Task +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test + +class StatisticsUtilsTest{ + + @Test + fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() { + // Create an active task + val task = Task("My test task","Im testing with JUnit4") + val tasks = listOf(task) + // Call your function + val statResults = getActiveAndCompletedStats(tasks) + // Check the result + assertThat(statResults.activeTasksPercent, `is`(100f)) + assertThat(statResults.completedTasksPercent, `is`(0f)) + } + + @Test + fun getActiveAndCompletedStats_oneTaskCompleted_noActiveTasks_returnsZeroHundred() { + // Create an active task + val task = Task("My test task","one task completed and no active tasks",true) + val tasks = listOf(task) + // Call your function + val statResults = getActiveAndCompletedStats(tasks) + // Check the result + assertThat(statResults.activeTasksPercent, `is`(0f)) + assertThat(statResults.completedTasksPercent, `is`(100f)) + } + + @Test + fun getActiveAndCompletedStats_twoTasksCompleted_threeActiveTasks_returnsZeroHundred() { + // Create an active task + val task1 = Task("My test task","Two completed tasks and three active tasks",true) + val task2 = Task("My test task","Two completed tasks and three active tasks",true) + val task3 = Task("My test task","Two completed tasks and three active tasks") + val task4 = Task("My test task","Two completed tasks and three active tasks") + val task5 = Task("My test task","Two completed tasks and three active tasks") + + val tasks = listOf(task1,task2,task3,task4,task5) + // Call your function + val statResults = getActiveAndCompletedStats(tasks) + // Check the result + assertThat(statResults.activeTasksPercent, `is`(60f)) + assertThat(statResults.completedTasksPercent, `is`(40f)) + } + + @Test + fun getActiveAndCompletedStats_null_returnsZeroZero() { + // Create an active task + val tasks = listOf() + // Call your function + val statResults = getActiveAndCompletedStats(tasks) + // Check the result + assertThat(statResults.activeTasksPercent, `is`(0f)) + assertThat(statResults.completedTasksPercent, `is`(0f)) + } + + @Test + fun getActiveAndCompletedStats_emptyList_returnsZeroZero() { + // Call your function + val statResults = getActiveAndCompletedStats(emptyList()) + // Check the result + assertThat(statResults.activeTasksPercent, `is`(0f)) + assertThat(statResults.completedTasksPercent, `is`(0f)) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index ec29cbb07..4bb304e50 100644 --- a/build.gradle +++ b/build.gradle @@ -48,4 +48,5 @@ ext { rulesVersion = '1.0.1' swipeRefreshLayoutVersion = '1.1.0' timberVersion = '4.7.1' + hamcrestVersion = '2.2' } From a6025abad6a88dd4152b4a60e15478815639a35b Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Wed, 1 Mar 2023 18:00:03 -0500 Subject: [PATCH 03/10] Settings up a ViewModel Test with AndroidX Test --- app/build.gradle | 44 +++++++++++++++++++ .../todoapp/tasks/TasksViewModelTest.kt | 25 +++++++++++ build.gradle | 2 + gradle.properties | 1 + 4 files changed, 72 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 9044e0dba..601ab2ba2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,6 +25,13 @@ android { enabled = true enabledForTests = true } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + } dependencies { @@ -40,6 +47,8 @@ dependencies { // Architecture Components implementation "androidx.room:room-runtime:$roomVersion" + implementation 'androidx.test.ext:junit-ktx:1.1.5' + testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12' @@ -66,4 +75,39 @@ dependencies { // Other dependencies testImplementation "org.hamcrest:hamcrest:$hamcrestVersion" + + + // AndroidX Test - JVM testing + testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion" + + + // Core library + androidTestImplementation "androidx.test:core:1.5.0" + + // To use the androidx.test.core APIs + androidTestImplementation "androidx.test:core:1.5.0" + // Kotlin extensions for androidx.test.core + androidTestImplementation "androidx.test:core-ktx:1.5.0" + + // To use the androidx.test.espresso + androidTestImplementation "androidx.test:espresso:espresso-core:3.5.1" + + // To use the JUnit Extension APIs + androidTestImplementation "androidx.test.ext:junit:1.1.5" + // Kotlin extensions for androidx.test.ext.junit + androidTestImplementation "androidx.test.ext:junit-ktx:1.1.5" + + // To use the Truth Extension APIs + androidTestImplementation "androidx.test.ext:truth:1.5.0" + + // To use the androidx.test.runner APIs + androidTestImplementation "androidx.test:runner:1.5.2" + + // To use android test orchestrator + androidTestUtil "androidx.test:orchestrator:1.4.2" + + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.9' + + } 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..bbcd347c3 --- /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.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TasksViewModelTest{ + + @Test + fun addNewTask_setsNewTaskEvent() { + + // Given a fresh TasksViewModel + val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) + + // When adding a new task + tasksViewModel.addNewTask() + + // Then the new task event is triggered + + } +} diff --git a/build.gradle b/build.gradle index 4bb304e50..e18547d02 100644 --- a/build.gradle +++ b/build.gradle @@ -49,4 +49,6 @@ ext { swipeRefreshLayoutVersion = '1.1.0' timberVersion = '4.7.1' hamcrestVersion = '2.2' + robolectricVersion = '4.5.1' + testRunnerVersion = '1.5.2' } diff --git a/gradle.properties b/gradle.properties index acf164f6c..80561005e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,3 +18,4 @@ # org.gradle.parallel=true android.enableJetifier=true android.useAndroidX=true + From d756a84ada801b1dc502780bc1ee624ff579810f Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Thu, 2 Mar 2023 11:41:45 -0500 Subject: [PATCH 04/10] Writing Assertions for LiveData --- app/build.gradle | 3 ++ .../blueprints/todoapp/LiveDataTestUtil.kt | 41 +++++++++++++++++++ .../todoapp/tasks/TasksViewModelTest.kt | 40 ++++++++++++++++-- build.gradle | 1 + 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/LiveDataTestUtil.kt diff --git a/app/build.gradle b/app/build.gradle index 601ab2ba2..9dc177f3e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,5 +109,8 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.9' + //To use InstantTaskExecutorRule + testImplementation "androidx.arch.core:core-testing:$archTestingVersion" + } diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/LiveDataTestUtil.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/LiveDataTestUtil.kt new file mode 100644 index 000000000..97c7a0b92 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/LiveDataTestUtil.kt @@ -0,0 +1,41 @@ +package com.example.android.architecture.blueprints.todoapp + +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +@VisibleForTesting(otherwise = VisibleForTesting.NONE) +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS, + afterObserve: () -> Unit = {} +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + this.observeForever(observer) + + try { + afterObserve.invoke() + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") + } + + } finally { + this.removeObserver(observer) + } + + @Suppress("UNCHECKED_CAST") + 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 bbcd347c3..69001adc2 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,25 +1,59 @@ package com.example.android.architecture.blueprints.todoapp.tasks +import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue +import org.hamcrest.CoreMatchers.* +import org.hamcrest.MatcherAssert.assertThat import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TasksViewModelTest{ + // Subject under test + private lateinit var tasksViewModel: TasksViewModel + + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + + @Before + fun setupViewModel() { + tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) + } + @Test fun addNewTask_setsNewTaskEvent() { - // Given a fresh TasksViewModel - val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) - // When adding a new task tasksViewModel.addNewTask() // Then the new task event is triggered + val value = tasksViewModel.newTaskEvent.getOrAwaitValue() + + // Assert that the value is not null + assertThat(value.getContentIfNotHandled(), (not(nullValue()))) + + } + + @Test + fun setFilterAllTasks_tasksAddViewVisible() { + + // set filtering to ALL_TASKS + tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS) + + // Then the new task event is triggered + val value = tasksViewModel.tasksAddViewVisible.getOrAwaitValue() + + // Assert that the value is not null + assertThat(value, `is` (true)) } } diff --git a/build.gradle b/build.gradle index e18547d02..67f06f56a 100644 --- a/build.gradle +++ b/build.gradle @@ -51,4 +51,5 @@ ext { hamcrestVersion = '2.2' robolectricVersion = '4.5.1' testRunnerVersion = '1.5.2' + archTestingVersion = '2.2.0' } From b034c97bb7d8e47fd9f88e3ed9cb2375965bc7ba Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Fri, 3 Mar 2023 12:40:26 -0500 Subject: [PATCH 05/10] Setup a fake Data source and test using DI --- app/build.gradle | 3 + .../data/source/DefaultTasksRepository.kt | 21 +++--- .../data/source/DefaultTasksRepositoryTest.kt | 50 +++++++++++++ .../todoapp/data/source/FakeDataSource.kt | 72 +++++++++++++++++++ 4 files changed, 133 insertions(+), 13 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 9dc177f3e..92dde0ec1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -112,5 +112,8 @@ dependencies { //To use InstantTaskExecutorRule testImplementation "androidx.arch.core:core-testing:$archTestingVersion" + // test with coroutines + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" + } 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..3d3c810cc 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,11 +33,11 @@ import kotlinx.coroutines.withContext /** * 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,21 +45,16 @@ 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) { 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..df15e3373 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt @@ -0,0 +1,50 @@ +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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.core.IsEqual +import org.junit.Before +import org.junit.Test + + +@ExperimentalCoroutinesApi +class DefaultTasksRepositoryTest{ + + private val task1 = Task("Title1", "Description1") + private val task2 = Task("Title2", "Description2") + private val task3 = Task("Title3", "Description3") + private val remoteTasks = listOf(task1, task2).sortedBy { it.id } + private val localTasks = listOf(task3).sortedBy { it.id } + private val newTasks = listOf(task3).sortedBy { it.id } + + private lateinit var tasksRemoteDataSource: FakeDataSource + private lateinit var tasksLocalDataSource: FakeDataSource + + // Class under test + private lateinit var tasksRepository: DefaultTasksRepository + + @Before + fun createRepository() { + tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList()) + tasksLocalDataSource = FakeDataSource(localTasks.toMutableList()) + // Get a reference to the class under test + tasksRepository = DefaultTasksRepository( + // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main + // this requires understanding more about coroutines + testing + // so we will keep this as Unconfined for now. + tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined + ) + } + + @Test + fun getTasks_requestsAllTasksFromRemoteDataSource() = runTest { + + val tasks = tasksRepository.getTasks(true) as Result.Success + assertThat(tasks.data, IsEqual(remoteTasks)) + + } +} \ 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..0347efc29 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDataSource.kt @@ -0,0 +1,72 @@ +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 +import com.example.android.architecture.blueprints.todoapp.data.Result.Error +import com.example.android.architecture.blueprints.todoapp.data.Result.Success + +class FakeDataSource(var tasks: MutableList? = mutableListOf()) : TasksDataSource{ + override fun observeTasks(): LiveData>> { + TODO("Not yet implemented") + } + + override suspend fun getTasks(): Result> { + + return if(tasks == null){ + Error(Exception("Null tasks object")) + }else{ + Success(tasks!!) + } + + } + + 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) { + val task = tasks?.find { task -> task.id == taskId } + tasks?.remove(task!!) + } +} \ No newline at end of file From 85fcacd5ad8e79f5ecdae5d2b146638a0aca5c9d Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Sat, 4 Mar 2023 11:39:18 -0500 Subject: [PATCH 06/10] Use the Fake repository inside ViewModels --- .../data/source/DefaultTasksRepository.kt | 30 +++---- .../todoapp/data/source/TasksRepository.kt | 36 ++++++++ .../todoapp/taskdetail/TaskDetailFragment.kt | 5 +- .../todoapp/taskdetail/TaskDetailViewModel.kt | 14 +++- .../blueprints/todoapp/tasks/TasksFragment.kt | 5 +- .../todoapp/tasks/TasksViewModel.kt | 14 +++- .../data/source/DefaultTasksRepositoryTest.kt | 13 ++- .../todoapp/data/source/FakeTestRepository.kt | 84 +++++++++++++++++++ .../taskdetail/TaskDetailViewModelTest.kt | 74 ++++++++++++++++ .../todoapp/tasks/TasksViewModelTest.kt | 19 +++-- 10 files changed, 262 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksRepository.kt create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeTestRepository.kt create mode 100644 app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.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 3d3c810cc..79dbf19af 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 @@ -37,7 +37,7 @@ class DefaultTasksRepository( private val tasksRemoteDataSource: TasksDataSource, private val tasksLocalDataSource: TasksDataSource, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO -) { +) : TasksRepository { companion object { @Volatile @@ -56,7 +56,7 @@ class DefaultTasksRepository( } - suspend fun getTasks(forceUpdate: Boolean = false): Result> { + override suspend fun getTasks(forceUpdate: Boolean): Result> { if (forceUpdate) { try { updateTasksFromRemoteDataSource() @@ -67,15 +67,15 @@ 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) } @@ -93,7 +93,7 @@ class DefaultTasksRepository( } } - fun observeTask(taskId: String): LiveData> { + override fun observeTask(taskId: String): LiveData> { return tasksLocalDataSource.observeTask(taskId) } @@ -108,28 +108,28 @@ 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) } return tasksLocalDataSource.getTask(taskId) } - suspend fun saveTask(task: Task) { + override suspend fun saveTask(task: Task) { coroutineScope { launch { tasksRemoteDataSource.saveTask(task) } launch { tasksLocalDataSource.saveTask(task) } } } - 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) @@ -137,14 +137,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) @@ -152,14 +152,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() } @@ -168,7 +168,7 @@ class DefaultTasksRepository( } } - suspend fun deleteTask(taskId: String) { + override suspend fun deleteTask(taskId: String) { coroutineScope { launch { tasksRemoteDataSource.deleteTask(taskId) } launch { tasksLocalDataSource.deleteTask(taskId) } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksRepository.kt new file mode 100644 index 000000000..6291f869d --- /dev/null +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksRepository.kt @@ -0,0 +1,36 @@ +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 TasksRepository { + suspend fun getTasks(forceUpdate: Boolean = false): Result> + + suspend fun refreshTasks() + fun observeTasks(): LiveData>> + + suspend fun refreshTask(taskId: String) + fun observeTask(taskId: String): LiveData> + + /** + * 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 saveTask(task: Task) + + 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) +} \ No newline at end of file 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..4420488a2 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 @@ -28,6 +28,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.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 +43,9 @@ 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..012a8c3de 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 @@ -24,16 +24,14 @@ 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.TasksRepository import kotlinx.coroutines.launch /** * ViewModel for the Details screen. */ -class TaskDetailViewModel(application: Application) : AndroidViewModel(application) { +class TaskDetailViewModel(private val tasksRepository: TasksRepository) : 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: TasksRepository +) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class) = + (TaskDetailViewModel(tasksRepository) as T) +} \ No newline at end of file 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..37d76ad3c 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 @@ -31,6 +31,7 @@ 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 @@ -43,7 +44,9 @@ import timber.log.Timber */ class TasksFragment : Fragment() { - private val viewModel by viewModels() + private val viewModel by viewModels{ + TasksViewModelFactory(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..825f2c4e3 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 @@ -26,16 +26,14 @@ 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.TasksDataSource +import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository import kotlinx.coroutines.launch /** * ViewModel for the task list screen. */ -class TasksViewModel(application: Application) : AndroidViewModel(application) { +class TasksViewModel(private val tasksRepository: TasksRepository) : 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 +229,11 @@ class TasksViewModel(application: Application) : AndroidViewModel(application) { _forceUpdate.value = true } } + +@Suppress("UNCHECKED_CAST") +class TasksViewModelFactory ( + private val tasksRepository: TasksRepository +) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class) = + (TasksViewModel(tasksRepository) as T) +} \ No newline at end of file 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 index df15e3373..8c1aafb49 100644 --- 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 @@ -5,6 +5,7 @@ import com.example.android.architecture.blueprints.todoapp.data.Task import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual import org.junit.Before @@ -19,7 +20,8 @@ class DefaultTasksRepositoryTest{ private val task3 = Task("Title3", "Description3") private val remoteTasks = listOf(task1, task2).sortedBy { it.id } private val localTasks = listOf(task3).sortedBy { it.id } - private val newTasks = listOf(task3).sortedBy { it.id } + private val newTasks = listOf(task2,task3).sortedBy { it.id } + private lateinit var tasksRemoteDataSource: FakeDataSource private lateinit var tasksLocalDataSource: FakeDataSource @@ -47,4 +49,13 @@ class DefaultTasksRepositoryTest{ assertThat(tasks.data, IsEqual(remoteTasks)) } + + @Test + fun deleteTask_requestsAllTasksFromRemoteDataSource() = runTest { + + val tasks = tasksRepository.getTasks() as Result.Success + tasksRepository.deleteAllTasks() + assertThat(tasks.data.size, `is` (0)) + + } } \ No newline at end of file diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeTestRepository.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeTestRepository.kt new file mode 100644 index 000000000..b715b3a5c --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeTestRepository.kt @@ -0,0 +1,84 @@ +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 FakeTestRepository : TasksRepository{ + + var tasksServiceData: LinkedHashMap = LinkedHashMap() + + private val observableTasks = MutableLiveData>>() + + fun addTasks(vararg tasks: Task) { + for (task in tasks) { + tasksServiceData[task.id] = task + } + runBlocking { refreshTasks() } + } + + override suspend fun getTasks(forceUpdate: Boolean): Result> { + val tasksList = tasksServiceData.values.toList() + return Result.Success(tasksList) + } + + override suspend fun refreshTasks() { + observableTasks.value = getTasks() + } + + override fun observeTasks(): LiveData>> { + runBlocking { + refreshTasks() + } + return observableTasks + } + + override suspend fun refreshTask(taskId: String) { + TODO("Not yet implemented") + } + + override fun observeTask(taskId: String): LiveData> { + TODO("Not yet implemented") + } + + override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result { + TODO("Not yet implemented") + } + + override suspend fun saveTask(task: Task) { + TODO("Not yet implemented") + } + + override suspend fun completeTask(task: Task) { + val completedTask = task.copy(isCompleted = true) + tasksServiceData[task.id] = completedTask + 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") + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt new file mode 100644 index 000000000..0200a9232 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt @@ -0,0 +1,74 @@ +package com.example.android.architecture.blueprints.todoapp.taskdetail + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.example.android.architecture.blueprints.todoapp.data.Task +import com.example.android.architecture.blueprints.todoapp.data.source.FakeTestRepository +import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking +import org.hamcrest.CoreMatchers.* +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test + + +class TaskDetailViewModelTest{ + + // Use a fake repository to be injected into the viewmodel + private lateinit var tasksRepository: FakeTestRepository + + // Subject under test + private lateinit var taskDetailViewModel: TaskDetailViewModel + + // id of a random task for test + + private lateinit var taskId : String + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @Before + fun setupViewModel() { + // We initialise the tasks to 3, with one active and two completed + tasksRepository = FakeTestRepository() + val task1 = Task("Title1", "Description1") + val task2 = Task("Title2", "Description2", true) + val task3 = Task("Title3", "Description3", true) + taskId = task1.id + tasksRepository.addTasks(task1, task2, task3) + + taskDetailViewModel = TaskDetailViewModel(tasksRepository) + } + + + @Test + fun editTask_setsNewEditTaskEvent(){ + + taskDetailViewModel.editTask() + + val value = taskDetailViewModel.editTaskEvent.getOrAwaitValue() + + assertThat(value.getContentIfNotHandled(), (not(nullValue()))) + + } + +// @Test +// fun completeTask_setsNewCompletedTaskEvent() { +// +// val task = tasksRepository.tasksServiceData.toList() +// +// val randomTask = task[0].second +// +// taskDetailViewModel.start(randomTask.id) +// +// // set task as completed +// +// taskDetailViewModel.setCompleted(true) +// +// +// assertThat(randomTask.isCompleted, `is` (true)) +// +// } + +} \ 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 69001adc2..509123595 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 @@ -2,20 +2,20 @@ package com.example.android.architecture.blueprints.todoapp.tasks import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.example.android.architecture.blueprints.todoapp.data.Task +import com.example.android.architecture.blueprints.todoapp.data.source.FakeTestRepository import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat -import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) class TasksViewModelTest{ + // Use a fake repository to be injected into the viewmodel + private lateinit var tasksRepository: FakeTestRepository + // Subject under test private lateinit var tasksViewModel: TasksViewModel @@ -26,7 +26,14 @@ class TasksViewModelTest{ @Before fun setupViewModel() { - tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) + // We initialise the tasks to 3, with one active and two completed + tasksRepository = FakeTestRepository() + val task1 = Task("Title1", "Description1") + val task2 = Task("Title2", "Description2", true) + val task3 = Task("Title3", "Description3", true) + tasksRepository.addTasks(task1, task2, task3) + + tasksViewModel = TasksViewModel(tasksRepository) } @Test From 75acadf7470d1e7a41f475861f515307645e847b Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Tue, 7 Mar 2023 10:27:40 -0500 Subject: [PATCH 07/10] Make a ServiceLocator --- app/build.gradle | 14 ++- .../data/source/FakeAndroidTestRepository.kt | 115 ++++++++++++++++++ .../taskdetail/TaskDetailFragmentTest.kt | 48 ++++++++ .../blueprints/todoapp/TodoApplication.kt | 5 + .../addedittask/AddEditTaskViewModel.kt | 3 +- .../data/source/DefaultTasksRepository.kt | 16 --- .../todoapp/servicelocator/ServiceLocator.kt | 65 ++++++++++ .../todoapp/statistics/StatisticsViewModel.kt | 3 +- .../todoapp/taskdetail/TaskDetailFragment.kt | 3 +- .../blueprints/todoapp/tasks/TasksFragment.kt | 3 +- build.gradle | 1 + 11 files changed, 253 insertions(+), 23 deletions(-) create mode 100644 app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidTestRepository.kt create mode 100644 app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragmentTest.kt create mode 100644 app/src/main/java/com/example/android/architecture/blueprints/todoapp/servicelocator/ServiceLocator.kt diff --git a/app/build.gradle b/app/build.gradle index 92dde0ec1..f9978b203 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,6 +52,7 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12' + androidTestImplementation 'junit:junit:4.12' kapt "androidx.room:room-compiler:$roomVersion" implementation "androidx.room:room-ktx:$roomVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion" @@ -89,9 +90,6 @@ dependencies { // Kotlin extensions for androidx.test.core androidTestImplementation "androidx.test:core-ktx:1.5.0" - // To use the androidx.test.espresso - androidTestImplementation "androidx.test:espresso:espresso-core:3.5.1" - // To use the JUnit Extension APIs androidTestImplementation "androidx.test.ext:junit:1.1.5" // Kotlin extensions for androidx.test.ext.junit @@ -115,5 +113,15 @@ dependencies { // test with coroutines testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" + // Dependencies for Android instrumented unit tests + androidTestImplementation "junit:junit:$junitVersion" + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" + + // Testing code should not be included in the main code. + // Once https://issuetracker.google.com/128612536 is fixed this can be fixed. + + debugImplementation "androidx.fragment:fragment-testing:$fragmentVersion" + implementation "androidx.test:core:$androidXTestCoreVersion" + } diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidTestRepository.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidTestRepository.kt new file mode 100644 index 000000000..01ee41179 --- /dev/null +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeAndroidTestRepository.kt @@ -0,0 +1,115 @@ +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.Result.Error +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.TasksRepository +import kotlinx.coroutines.runBlocking + + +class FakeAndroidTestRepository : TasksRepository { + + 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 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 Error -> Error(tasks.exception) + is Success -> { + val task = tasks.data.firstOrNull() { it.id == taskId } + ?: return@map Error(Exception("Not found")) + Success(task) + } + } + } + } + + override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result { + if (shouldReturnError) { + return Error(Exception("Test exception")) + } + tasksServiceData[taskId]?.let { + return Success(it) + } + return Error(Exception("Could not find task")) + } + + override suspend fun getTasks(forceUpdate: Boolean): Result> { + if (shouldReturnError) { + return Error(Exception("Test exception")) + } + return 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 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 new file mode 100644 index 000000000..14227afe0 --- /dev/null +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragmentTest.kt @@ -0,0 +1,48 @@ +package com.example.android.architecture.blueprints.todoapp.taskdetail + +import FakeAndroidTestRepository +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.data.Task +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import com.example.android.architecture.blueprints.todoapp.R +import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository +import com.example.android.architecture.blueprints.todoapp.servicelocator.ServiceLocator +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.After +import org.junit.Before + +@MediumTest +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class TaskDetailFragmentTest{ + + private lateinit var repository: TasksRepository + + @Before + fun initRepository() { + repository = FakeAndroidTestRepository() + ServiceLocator.tasksRepository = repository + } + + @After + fun cleanupDb() = runBlockingTest { + ServiceLocator.resetRepository() + } + + @Test + fun activeTaskDetails_DisplayedInUi() = runBlockingTest{ + // GIVEN - Add active (incomplete) task to the DB + val activeTask = Task("Active Task", "AndroidX Rocks", false) + repository.saveTask(activeTask) + + // WHEN - Details fragment launched to display task + val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle() + launchFragmentInContainer(bundle, R.style.AppTheme) + + } +} \ 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..c8a35ef68 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,8 @@ package com.example.android.architecture.blueprints.todoapp import android.app.Application +import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository +import com.example.android.architecture.blueprints.todoapp.servicelocator.ServiceLocator import timber.log.Timber import timber.log.Timber.DebugTree @@ -28,6 +30,9 @@ import timber.log.Timber.DebugTree */ class TodoApplication : Application() { + val taskRepository: TasksRepository + get() = ServiceLocator.provideTasksRepository(this) + 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..5db1248f0 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).taskRepository // 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 79dbf19af..8eab105b3 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 @@ -39,22 +39,6 @@ class DefaultTasksRepository( private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) : TasksRepository { - companion object { - @Volatile - private var INSTANCE: DefaultTasksRepository? = null - - 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 - } - } - } - } - override suspend fun getTasks(forceUpdate: Boolean): Result> { if (forceUpdate) { diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/servicelocator/ServiceLocator.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/servicelocator/ServiceLocator.kt new file mode 100644 index 000000000..ad261784b --- /dev/null +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/servicelocator/ServiceLocator.kt @@ -0,0 +1,65 @@ +package com.example.android.architecture.blueprints.todoapp.servicelocator + +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.TasksDataSource +import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository +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 + var tasksRepository: TasksRepository? = null + @VisibleForTesting set + + fun provideTasksRepository(context: Context): TasksRepository { + synchronized(this) { + return tasksRepository ?: createTasksRepository(context) + } + } + + private fun createTasksRepository(context: Context): TasksRepository { + val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context)) + tasksRepository = newRepo + return newRepo + } + + private fun createTaskLocalDataSource(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, "Tasks.db" + ).build() + database = result + return result + } + + @VisibleForTesting + fun resetRepository() { + synchronized(lock) { + runBlocking { + TasksRemoteDataSource.deleteAllTasks() + } + // Clear all data to avoid test pollution. + database?.apply { + clearAllTables() + close() + } + database = null + tasksRepository = null + } + } + +} \ 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..3a10059e3 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).taskRepository 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 4420488a2..bd75d47cc 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 @@ -28,6 +28,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 @@ -44,7 +45,7 @@ class TaskDetailFragment : Fragment() { private val args: TaskDetailFragmentArgs by navArgs() private val viewModel by viewModels{ - TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application)) + TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 37d76ad3c..7c2548576 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 @@ -30,6 +30,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 @@ -45,7 +46,7 @@ import timber.log.Timber class TasksFragment : Fragment() { private val viewModel by viewModels{ - TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application)) + TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository) } private val args: TasksFragmentArgs by navArgs() diff --git a/build.gradle b/build.gradle index 67f06f56a..8b6839867 100644 --- a/build.gradle +++ b/build.gradle @@ -52,4 +52,5 @@ ext { robolectricVersion = '4.5.1' testRunnerVersion = '1.5.2' archTestingVersion = '2.2.0' + fragmentVersion = '1.5.5' } From c0a448ab422a0eb8bf9133e7091206f18be8fabf Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Tue, 7 Mar 2023 12:15:06 -0500 Subject: [PATCH 08/10] Writing integration test with Espresso --- .../taskdetail/TaskDetailFragmentTest.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 14227afe0..ac979fb51 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 @@ -2,6 +2,9 @@ package com.example.android.architecture.blueprints.todoapp.taskdetail import FakeAndroidTestRepository import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.example.android.architecture.blueprints.todoapp.data.Task @@ -13,6 +16,7 @@ import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepo import com.example.android.architecture.blueprints.todoapp.servicelocator.ServiceLocator import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest +import org.hamcrest.CoreMatchers.not import org.junit.After import org.junit.Before @@ -44,5 +48,32 @@ class TaskDetailFragmentTest{ val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle() launchFragmentInContainer(bundle, R.style.AppTheme) + onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed())) + onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task"))) + onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed())) + onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks"))) + // and make sure the "active" checkbox is shown unchecked + onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed())) + onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked()))) + + } + + @Test + fun completedTaskDetails_DisplayedInUi() = runBlockingTest{ + // GIVEN - Add active (incomplete) task to the DB + val activeTask = Task("Active Task 2", "AndroidX Rocks", true) + repository.saveTask(activeTask) + + // WHEN - Details fragment launched to display task + val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle() + launchFragmentInContainer(bundle, R.style.AppTheme) + + onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed())) + onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task 2"))) + onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed())) + onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks"))) + // and make sure the "active" checkbox is shown unchecked + onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed())) + onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked())) } } \ No newline at end of file From d8139b00e4d25f9c63d172fd7ad90d15f85b5699 Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Wed, 8 Mar 2023 11:30:21 -0500 Subject: [PATCH 09/10] Using Mockito to write Navigation tests --- app/build.gradle | 18 +++-- .../todoapp/tasks/TasksFragmentTest.kt | 74 +++++++++++++++++++ .../taskdetail/TaskDetailViewModelTest.kt | 5 +- build.gradle | 10 ++- 4 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragmentTest.kt diff --git a/app/build.gradle b/app/build.gradle index f9978b203..704bc9655 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,11 +48,12 @@ dependencies { // Architecture Components implementation "androidx.room:room-runtime:$roomVersion" implementation 'androidx.test.ext:junit-ktx:1.1.5' - testImplementation 'junit:junit:4.12' - testImplementation 'junit:junit:4.12' - testImplementation 'junit:junit:4.12' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'junit:junit:4.12' + implementation 'androidx.test.espresso:espresso-contrib:3.5.1' + testImplementation 'junit:junit:4.13.2' + testImplementation 'junit:junit:4.13.2' + testImplementation 'junit:junit:4.13.2' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'junit:junit:4.13.2' kapt "androidx.room:room-compiler:$roomVersion" implementation "androidx.room:room-ktx:$roomVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion" @@ -105,7 +106,7 @@ dependencies { androidTestUtil "androidx.test:orchestrator:1.4.2" testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.9' + testImplementation 'org.robolectric:robolectric:4.9.2' //To use InstantTaskExecutorRule testImplementation "androidx.arch.core:core-testing:$archTestingVersion" @@ -123,5 +124,10 @@ dependencies { debugImplementation "androidx.fragment:fragment-testing:$fragmentVersion" implementation "androidx.test:core:$androidXTestCoreVersion" + // Dependencies for Android instrumented unit tests + androidTestImplementation "org.mockito:mockito-core:$mockitoVersion" + + androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" + } diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragmentTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragmentTest.kt new file mode 100644 index 000000000..31f45381e --- /dev/null +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragmentTest.kt @@ -0,0 +1,74 @@ +package com.example.android.architecture.blueprints.todoapp.tasks + +import FakeAndroidTestRepository +import android.os.Bundle +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.navigation.NavController +import androidx.navigation.Navigation +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.example.android.architecture.blueprints.todoapp.data.Task +import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository +import com.example.android.architecture.blueprints.todoapp.servicelocator.ServiceLocator +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.runner.RunWith +import com.example.android.architecture.blueprints.todoapp.R +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +@MediumTest +@ExperimentalCoroutinesApi +class TasksFragmentTest{ + + private lateinit var repository: TasksRepository + + @Before + fun initRepository() { + repository = FakeAndroidTestRepository() + ServiceLocator.tasksRepository = repository + } + + @After + fun cleanupDb() = runBlockingTest { + ServiceLocator.resetRepository() + } + + @Test + fun clickTask_navigateToDetailFragmentOne() = runBlockingTest { + repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1")) + repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2")) + + // GIVEN - On the home screen + val scenario = launchFragmentInContainer(Bundle(), R.style.AppTheme) + + val navController = mock(NavController::class.java) + + scenario.onFragment { + Navigation.setViewNavController(it.view!!, navController) + } + + // WHEN - Click on the first list item + onView(withId(R.id.tasks_list)) + .perform( + RecyclerViewActions.actionOnItem( + hasDescendant(withText("TITLE1")), click())) + + + // THEN - Verify that we navigate to the first detail screen + verify(navController).navigate( + TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")) + + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt index 0200a9232..fb28283f9 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt @@ -4,9 +4,8 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.FakeTestRepository import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.runBlocking -import org.hamcrest.CoreMatchers.* +import org.hamcrest.CoreMatchers.not +import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat import org.junit.Before import org.junit.Rule diff --git a/build.gradle b/build.gradle index 8b6839867..f8af10abb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext.kotlinVersion = '1.5.31' - ext.navigationVersion = '2.4.1' + ext.navigationVersion = '2.5.3' repositories { google() mavenCentral() @@ -31,15 +31,15 @@ ext { // App dependencies androidXVersion = '1.0.0' - androidXTestCoreVersion = '1.3.0' + androidXTestCoreVersion = '1.5.0' androidXTestExtKotlinRunnerVersion = '1.1.3' androidXTestRulesVersion = '1.2.0' androidXAnnotations = '1.3.0' appCompatVersion = '1.4.0' - archLifecycleVersion = '2.4.0' + archLifecycleVersion = '2.5.1' coroutinesVersion = '1.5.2' cardVersion = '1.0.0' - espressoVersion = '3.4.0' + espressoVersion = '3.5.1' fragmentKtxVersion = '1.4.0' junitVersion = '4.13.2' materialVersion = '1.4.0' @@ -53,4 +53,6 @@ ext { testRunnerVersion = '1.5.2' archTestingVersion = '2.2.0' fragmentVersion = '1.5.5' + mockitoVersion = '5.1.1' + dexMakerVersion = '2.28.3' } From ed1ee1ee9cce0bd904f5d1eef06f5e196be3cf84 Mon Sep 17 00:00:00 2001 From: med25ch <1804233@bdeb.qc.ca> Date: Wed, 8 Mar 2023 11:30:34 -0500 Subject: [PATCH 10/10] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 293b1b80f..eae9f3d09 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build .project .settings/ .classpath +java_pid55928.hprof