Skip to content
This repository was archived by the owner on Oct 25, 2025. It is now read-only.

Commit 7407f2e

Browse files
committed
End of codelab 2
1 parent e2c4112 commit 7407f2e

File tree

18 files changed

+858
-89
lines changed

18 files changed

+858
-89
lines changed

app/build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ dependencies {
6262
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
6363
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
6464
testImplementation "org.robolectric:robolectric:$robolectricVersion"
65+
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
66+
67+
// Dependencies for Android instrumented unit tests
68+
androidTestImplementation "junit:junit:$junitVersion"
69+
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
70+
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
71+
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
72+
73+
// Testing code should not be included in the main code.
74+
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
75+
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
76+
implementation "androidx.test:core:$androidXTestCoreVersion"
6577

6678
// AndroidX Test - JVM testing
6779
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
@@ -70,6 +82,7 @@ dependencies {
7082
// AndroidX Test - Instrumented testing
7183
androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion"
7284
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
85+
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
7386

7487
// Kotlin
7588
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright (C) 2019 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.android.architecture.blueprints.todoapp.data.source
17+
18+
import androidx.lifecycle.LiveData
19+
import androidx.lifecycle.MutableLiveData
20+
import androidx.lifecycle.map
21+
import com.example.android.architecture.blueprints.todoapp.data.Result
22+
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
23+
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
24+
import com.example.android.architecture.blueprints.todoapp.data.Task
25+
import kotlinx.coroutines.runBlocking
26+
import java.util.LinkedHashMap
27+
28+
/**
29+
* Implementation of a remote data source with static access to the data for easy testing.
30+
*/
31+
class FakeAndroidTestRepository : TasksRepository {
32+
33+
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
34+
35+
private val observableTasks = MutableLiveData<Result<List<Task>>>()
36+
37+
38+
override suspend fun refreshTasks() {
39+
observableTasks.value = getTasks()
40+
}
41+
42+
override suspend fun refreshTask(taskId: String) {
43+
refreshTasks()
44+
}
45+
46+
override fun observeTasks(): LiveData<Result<List<Task>>> {
47+
runBlocking { refreshTasks() }
48+
return observableTasks
49+
}
50+
51+
override fun observeTask(taskId: String): LiveData<Result<Task>> {
52+
runBlocking { refreshTasks() }
53+
return observableTasks.map { tasks ->
54+
when (tasks) {
55+
is Result.Loading -> Result.Loading
56+
is Error -> Error(tasks.exception)
57+
is Success -> {
58+
val task = tasks.data.firstOrNull() { it.id == taskId }
59+
?: return@map Error(Exception("Not found"))
60+
Success(task)
61+
}
62+
}
63+
}
64+
}
65+
66+
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
67+
tasksServiceData[taskId]?.let {
68+
return Success(it)
69+
}
70+
return Error(Exception("Could not find task"))
71+
}
72+
73+
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
74+
return Success(tasksServiceData.values.toList())
75+
}
76+
77+
override suspend fun saveTask(task: Task) {
78+
tasksServiceData[task.id] = task
79+
}
80+
81+
override suspend fun completeTask(task: Task) {
82+
val completedTask = task.copy(isCompleted = true)
83+
tasksServiceData[task.id] = completedTask
84+
refreshTasks()
85+
}
86+
87+
override suspend fun completeTask(taskId: String) {
88+
// Not required for the remote data source.
89+
throw NotImplementedError()
90+
}
91+
92+
override suspend fun activateTask(task: Task) {
93+
val activeTask = task.copy(isCompleted = false)
94+
tasksServiceData[task.id] = activeTask
95+
refreshTasks()
96+
}
97+
98+
override suspend fun activateTask(taskId: String) {
99+
throw NotImplementedError()
100+
}
101+
102+
override suspend fun clearCompletedTasks() {
103+
tasksServiceData = tasksServiceData.filterValues {
104+
!it.isCompleted
105+
} as LinkedHashMap<String, Task>
106+
}
107+
108+
override suspend fun deleteTask(taskId: String) {
109+
tasksServiceData.remove(taskId)
110+
refreshTasks()
111+
}
112+
113+
override suspend fun deleteAllTasks() {
114+
tasksServiceData.clear()
115+
refreshTasks()
116+
}
117+
118+
fun addTasks(vararg tasks: Task) {
119+
for (task in tasks) {
120+
tasksServiceData[task.id] = task
121+
}
122+
runBlocking { refreshTasks() }
123+
}
124+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright (C) 2019 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.android.architecture.blueprints.todoapp.taskdetail
17+
18+
import androidx.fragment.app.testing.launchFragmentInContainer
19+
import androidx.test.espresso.Espresso.onView
20+
import androidx.test.espresso.assertion.ViewAssertions.matches
21+
import androidx.test.espresso.matcher.ViewMatchers.isChecked
22+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
23+
import androidx.test.espresso.matcher.ViewMatchers.withId
24+
import androidx.test.espresso.matcher.ViewMatchers.withText
25+
import androidx.test.ext.junit.runners.AndroidJUnit4
26+
import androidx.test.filters.MediumTest
27+
import com.example.android.architecture.blueprints.todoapp.R
28+
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
29+
import com.example.android.architecture.blueprints.todoapp.data.Task
30+
import com.example.android.architecture.blueprints.todoapp.data.source.FakeAndroidTestRepository
31+
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
32+
import kotlinx.coroutines.ExperimentalCoroutinesApi
33+
import kotlinx.coroutines.test.runBlockingTest
34+
import org.hamcrest.core.IsNot.not
35+
import org.junit.After
36+
import org.junit.Before
37+
import org.junit.Test
38+
import org.junit.runner.RunWith
39+
40+
/**
41+
* Integration test for the Task Details screen.
42+
*/
43+
@MediumTest
44+
@RunWith(AndroidJUnit4::class)
45+
@ExperimentalCoroutinesApi
46+
class TaskDetailFragmentTest {
47+
48+
private lateinit var repository: TasksRepository
49+
50+
@Before
51+
fun initRepository() {
52+
repository = FakeAndroidTestRepository()
53+
ServiceLocator.tasksRepository = repository
54+
}
55+
56+
@After
57+
fun cleanupDb() = runBlockingTest {
58+
ServiceLocator.resetRepository()
59+
}
60+
61+
@Test
62+
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
63+
// GIVEN - Add active (incomplete) task to the DB
64+
val activeTask = Task("Active Task", "AndroidX Rocks", false)
65+
repository.saveTask(activeTask)
66+
67+
// WHEN - Details fragment launched to display task
68+
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
69+
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
70+
71+
// THEN - Task details are displayed on the screen
72+
// make sure that the title/description are both shown and correct
73+
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
74+
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
75+
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
76+
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
77+
// and make sure the "active" checkbox is shown unchecked
78+
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
79+
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
80+
}
81+
82+
@Test
83+
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
84+
// GIVEN - Add completed task to the DB
85+
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
86+
repository.saveTask(completedTask)
87+
88+
// WHEN - Details fragment launched to display task
89+
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
90+
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
91+
92+
// THEN - Task details are displayed on the screen
93+
// make sure that the title/description are both shown and correct
94+
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
95+
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
96+
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
97+
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
98+
// and make sure the "active" checkbox is shown unchecked
99+
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
100+
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
101+
}
102+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (C) 2019 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.android.architecture.blueprints.todoapp.tasks
18+
19+
import android.content.Context
20+
import android.os.Bundle
21+
import androidx.fragment.app.testing.launchFragmentInContainer
22+
import androidx.navigation.NavController
23+
import androidx.navigation.Navigation
24+
import androidx.recyclerview.widget.RecyclerView
25+
import androidx.test.core.app.ApplicationProvider.getApplicationContext
26+
import androidx.test.espresso.Espresso.onView
27+
import androidx.test.espresso.action.ViewActions.click
28+
import androidx.test.espresso.contrib.RecyclerViewActions
29+
import androidx.test.espresso.matcher.ViewMatchers.*
30+
import androidx.test.ext.junit.runners.AndroidJUnit4
31+
import androidx.test.filters.MediumTest
32+
import com.example.android.architecture.blueprints.todoapp.R
33+
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
34+
import com.example.android.architecture.blueprints.todoapp.data.Task
35+
import com.example.android.architecture.blueprints.todoapp.data.source.FakeAndroidTestRepository
36+
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
37+
import kotlinx.coroutines.ExperimentalCoroutinesApi
38+
import kotlinx.coroutines.test.runBlockingTest
39+
import org.junit.After
40+
import org.junit.Before
41+
import org.junit.Test
42+
import org.junit.runner.RunWith
43+
import org.mockito.Mockito.mock
44+
import org.mockito.Mockito.verify
45+
46+
/**
47+
* Integration test for the Task List screen.
48+
*/
49+
// TODO - Use FragmentScenario, see: https://github.com/android/android-test/issues/291
50+
@RunWith(AndroidJUnit4::class)
51+
@MediumTest
52+
@ExperimentalCoroutinesApi
53+
class TasksFragmentTest {
54+
55+
private lateinit var repository: TasksRepository
56+
57+
@Before
58+
fun initRepository() {
59+
repository = FakeAndroidTestRepository()
60+
ServiceLocator.tasksRepository = repository
61+
}
62+
63+
@After
64+
fun cleanupDb() = runBlockingTest {
65+
ServiceLocator.resetRepository()
66+
}
67+
68+
@Test
69+
fun clickAddTaskButton_navigateToAddEditFragment() {
70+
// GIVEN - On the home screen
71+
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
72+
val navController = mock(NavController::class.java)
73+
scenario.onFragment {
74+
Navigation.setViewNavController(it.view!!, navController)
75+
}
76+
77+
// WHEN - Click on the "+" button
78+
onView(withId(R.id.add_task_fab)).perform(click())
79+
80+
// THEN - Verify that we navigate to the add screen
81+
verify(navController).navigate(
82+
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
83+
null, getApplicationContext<Context>().getString(R.string.add_task)
84+
)
85+
)
86+
}
87+
88+
@Test
89+
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
90+
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
91+
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
92+
93+
// GIVEN - On the home screen
94+
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
95+
val navController = mock(NavController::class.java)
96+
scenario.onFragment {
97+
Navigation.setViewNavController(it.view!!, navController)
98+
}
99+
100+
// WHEN - Click on the first list item
101+
onView(withId(R.id.tasks_list))
102+
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
103+
hasDescendant(withText("TITLE1")), click()))
104+
105+
106+
// THEN - Verify that we navigate to the first detail screen
107+
verify(navController).navigate(
108+
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
109+
)
110+
}
111+
112+
}

0 commit comments

Comments
 (0)