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 diff --git a/app/build.gradle b/app/build.gradle index edea69366..704bc9655 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,13 @@ dependencies { // Architecture Components implementation "androidx.room:room-runtime:$roomVersion" + implementation 'androidx.test.ext:junit-ktx:1.1.5' + 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" @@ -60,4 +74,60 @@ dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion" + + // 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 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.2' + + //To use InstantTaskExecutorRule + testImplementation "androidx.arch.core:core-testing:$archTestingVersion" + + // 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" + + // 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/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..ac979fb51 --- /dev/null +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragmentTest.kt @@ -0,0 +1,79 @@ +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 +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.hamcrest.CoreMatchers.not +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) + + 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 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/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 a4123e54c..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 @@ -33,35 +33,14 @@ 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 +) : TasksRepository { - companion object { - @Volatile - private var INSTANCE: DefaultTasksRepository? = null - - fun getRepository(app: Application): DefaultTasksRepository { - return INSTANCE ?: synchronized(this) { - DefaultTasksRepository(app).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> { + override suspend fun getTasks(forceUpdate: Boolean): Result> { if (forceUpdate) { try { updateTasksFromRemoteDataSource() @@ -72,15 +51,15 @@ class DefaultTasksRepository private constructor(application: Application) { 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) } @@ -98,7 +77,7 @@ class DefaultTasksRepository private constructor(application: Application) { } } - fun observeTask(taskId: String): LiveData> { + override fun observeTask(taskId: String): LiveData> { return tasksLocalDataSource.observeTask(taskId) } @@ -113,28 +92,28 @@ 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 { + 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) @@ -142,14 +121,14 @@ class DefaultTasksRepository private constructor(application: Application) { } } - 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) @@ -157,14 +136,14 @@ class DefaultTasksRepository private constructor(application: Application) { } } - 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() } @@ -173,7 +152,7 @@ class DefaultTasksRepository private constructor(application: Application) { } } - 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/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/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/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 399436588..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,8 @@ 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 import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout @@ -42,7 +44,9 @@ class TaskDetailFragment : Fragment() { private val args: TaskDetailFragmentArgs by navArgs() - private val viewModel by viewModels() + private val viewModel by viewModels{ + TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository) + } 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..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,7 +30,9 @@ 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 import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout import com.example.android.architecture.blueprints.todoapp.util.setupSnackbar @@ -43,7 +45,9 @@ import timber.log.Timber */ class TasksFragment : Fragment() { - private val viewModel by viewModels() + private val viewModel by viewModels{ + TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository) + } 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/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/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..8c1aafb49 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt @@ -0,0 +1,61 @@ +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.CoreMatchers.`is` +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(task2,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)) + + } + + @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/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 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/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/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..fb28283f9 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt @@ -0,0 +1,73 @@ +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 org.hamcrest.CoreMatchers.not +import org.hamcrest.CoreMatchers.nullValue +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 new file mode 100644 index 000000000..509123595 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt @@ -0,0 +1,66 @@ +package com.example.android.architecture.blueprints.todoapp.tasks + + +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 org.hamcrest.CoreMatchers.* +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +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 + + + @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) + tasksRepository.addTasks(task1, task2, task3) + + tasksViewModel = TasksViewModel(tasksRepository) + } + + @Test + fun addNewTask_setsNewTaskEvent() { + + // 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 c851a4380..f8af10abb 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.5.3' 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,25 +27,32 @@ ext { // Sdk and tools minSdkVersion = 21 targetSdkVersion = 31 - compileSdkVersion = 31 + compileSdkVersion = 33 // 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' recyclerViewVersion = '1.2.1' - roomVersion = '2.3.0' + roomVersion = '2.5.0' rulesVersion = '1.0.1' swipeRefreshLayoutVersion = '1.1.0' timberVersion = '4.7.1' + hamcrestVersion = '2.2' + robolectricVersion = '4.5.1' + testRunnerVersion = '1.5.2' + archTestingVersion = '2.2.0' + fragmentVersion = '1.5.5' + mockitoVersion = '5.1.1' + dexMakerVersion = '2.28.3' } 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 + 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