Skip to content

Commit aa7e022

Browse files
committed
add testing fragment and ServiceLocator
1 parent d8d4919 commit aa7e022

File tree

9 files changed

+245
-6
lines changed

9 files changed

+245
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.example.android.architecture.blueprints.todoapp.data.source
2+
3+
import androidx.annotation.VisibleForTesting
4+
import androidx.lifecycle.LiveData
5+
import androidx.lifecycle.MutableLiveData
6+
import androidx.lifecycle.map
7+
import com.example.android.architecture.blueprints.todoapp.data.Result
8+
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
9+
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
10+
import com.example.android.architecture.blueprints.todoapp.data.Task
11+
import kotlinx.coroutines.runBlocking
12+
import java.util.LinkedHashMap
13+
14+
15+
class FakeAndroidTestRepository : TasksRepository {
16+
17+
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
18+
19+
private var shouldReturnError = false
20+
21+
private val observableTasks = MutableLiveData<Result<List<Task>>>()
22+
23+
fun setReturnError(value: Boolean) {
24+
shouldReturnError = value
25+
}
26+
27+
override suspend fun refreshTasks() {
28+
observableTasks.value = getTasks()
29+
}
30+
31+
override suspend fun refreshTask(taskId: String) {
32+
refreshTasks()
33+
}
34+
35+
override fun observeTasks(): LiveData<Result<List<Task>>> {
36+
runBlocking { refreshTasks() }
37+
return observableTasks
38+
}
39+
40+
override fun observeTask(taskId: String): LiveData<Result<Task>> {
41+
runBlocking { refreshTasks() }
42+
return observableTasks.map { tasks ->
43+
when (tasks) {
44+
is Result.Loading -> Result.Loading
45+
is Error -> Error(tasks.exception)
46+
is Success -> {
47+
val task = tasks.data.firstOrNull() { it.id == taskId }
48+
?: return@map Error(Exception("Not found"))
49+
Success(task)
50+
}
51+
}
52+
}
53+
}
54+
55+
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
56+
if (shouldReturnError) {
57+
return Error(Exception("Test exception"))
58+
}
59+
tasksServiceData[taskId]?.let {
60+
return Success(it)
61+
}
62+
return Error(Exception("Could not find task"))
63+
}
64+
65+
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
66+
if (shouldReturnError) {
67+
return Error(Exception("Test exception"))
68+
}
69+
return Success(tasksServiceData.values.toList())
70+
}
71+
72+
override suspend fun saveTask(task: Task) {
73+
tasksServiceData[task.id] = task
74+
}
75+
76+
override suspend fun completeTask(task: Task) {
77+
val completedTask = Task(task.title, task.description, true, task.id)
78+
tasksServiceData[task.id] = completedTask
79+
}
80+
81+
override suspend fun completeTask(taskId: String) {
82+
// Not required for the remote data source.
83+
throw NotImplementedError()
84+
}
85+
86+
override suspend fun activateTask(task: Task) {
87+
val activeTask = Task(task.title, task.description, false, task.id)
88+
tasksServiceData[task.id] = activeTask
89+
}
90+
91+
override suspend fun activateTask(taskId: String) {
92+
throw NotImplementedError()
93+
}
94+
95+
override suspend fun clearCompletedTasks() {
96+
tasksServiceData = tasksServiceData.filterValues {
97+
!it.isCompleted
98+
} as LinkedHashMap<String, Task>
99+
}
100+
101+
override suspend fun deleteTask(taskId: String) {
102+
tasksServiceData.remove(taskId)
103+
refreshTasks()
104+
}
105+
106+
override suspend fun deleteAllTasks() {
107+
tasksServiceData.clear()
108+
refreshTasks()
109+
}
110+
111+
112+
fun addTasks(vararg tasks: Task) {
113+
for (task in tasks) {
114+
tasksServiceData[task.id] = task
115+
}
116+
runBlocking { refreshTasks() }
117+
}
118+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.example.android.architecture.blueprints.todoapp.taskdetail
2+
3+
import androidx.fragment.app.testing.launchFragmentInContainer
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import androidx.test.filters.MediumTest
6+
import com.example.android.architecture.blueprints.todoapp.R
7+
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
8+
import com.example.android.architecture.blueprints.todoapp.data.Task
9+
import com.example.android.architecture.blueprints.todoapp.data.source.FakeAndroidTestRepository
10+
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
11+
import kotlinx.coroutines.ExperimentalCoroutinesApi
12+
import kotlinx.coroutines.test.runBlockingTest
13+
import org.junit.After
14+
import org.junit.Assert.*
15+
import org.junit.Before
16+
17+
import org.junit.Test
18+
import org.junit.runner.RunWith
19+
20+
@MediumTest
21+
@ExperimentalCoroutinesApi
22+
@RunWith(AndroidJUnit4::class)
23+
class TaskDetailFragmentTest {
24+
private lateinit var repository: TasksRepository
25+
26+
@Before
27+
fun initRepository() {
28+
repository = FakeAndroidTestRepository()
29+
ServiceLocator.tasksRepository = repository
30+
}
31+
32+
@After
33+
fun cleanupDb() = runBlockingTest {
34+
ServiceLocator.resetRepository()
35+
}
36+
37+
@Test
38+
fun activeTaskDetails_DisplayedInUi() = runBlockingTest {
39+
// Given - add active (incomplete) task to the DB
40+
val activeTask = Task("Active task", "Android rocks", false)
41+
repository.saveTask(activeTask)
42+
43+
// When - details fragment launched to display task
44+
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
45+
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
46+
}
47+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.example.android.architecture.blueprints.todoapp
2+
3+
import android.content.Context
4+
import androidx.annotation.VisibleForTesting
5+
import androidx.room.Room
6+
import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository
7+
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
8+
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
9+
import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksLocalDataSource
10+
import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase
11+
import com.example.android.architecture.blueprints.todoapp.data.source.remote.TasksRemoteDataSource
12+
import kotlinx.coroutines.runBlocking
13+
14+
object ServiceLocator {
15+
16+
private var database: ToDoDatabase? = null
17+
@Volatile
18+
var tasksRepository: TasksRepository? = null
19+
@VisibleForTesting set
20+
private val lock = Any()
21+
22+
fun provideTasksRepository(context: Context): TasksRepository {
23+
synchronized(this) {
24+
return tasksRepository ?: createTasksRepository(context)
25+
}
26+
}
27+
28+
private fun createTasksRepository(context: Context): TasksRepository {
29+
val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
30+
tasksRepository = newRepo
31+
return newRepo
32+
}
33+
34+
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
35+
val database = database ?: createDataBase(context)
36+
return TasksLocalDataSource(database.taskDao())
37+
}
38+
39+
private fun createDataBase(context: Context): ToDoDatabase {
40+
val result = Room.databaseBuilder(
41+
context.applicationContext,
42+
ToDoDatabase::class.java, "Tasks.db"
43+
).build()
44+
database = result
45+
return result
46+
}
47+
48+
@VisibleForTesting
49+
fun resetRepository() {
50+
synchronized(lock) {
51+
runBlocking {
52+
TasksRemoteDataSource.deleteAllTasks()
53+
}
54+
// Clear all data to avoid test pollution.
55+
database?.apply {
56+
clearAllTables()
57+
close()
58+
}
59+
database = null
60+
tasksRepository = null
61+
}
62+
}
63+
}

25-to-do-notes/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.example.android.architecture.blueprints.todoapp
1818

1919
import android.app.Application
20+
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
2021
import timber.log.Timber
2122
import timber.log.Timber.DebugTree
2223

@@ -27,6 +28,8 @@ import timber.log.Timber.DebugTree
2728
* Also, sets up Timber in the DEBUG BuildConfig. Read Timber's documentation for production setups.
2829
*/
2930
class TodoApplication : Application() {
31+
val taskRepository: TasksRepository
32+
get() = ServiceLocator.provideTasksRepository(this)
3033

3134
override fun onCreate() {
3235
super.onCreate()

25-to-do-notes/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import android.app.Application
2020
import androidx.lifecycle.*
2121
import com.example.android.architecture.blueprints.todoapp.Event
2222
import com.example.android.architecture.blueprints.todoapp.R
23+
import com.example.android.architecture.blueprints.todoapp.TodoApplication
2324
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
2425
import com.example.android.architecture.blueprints.todoapp.data.Task
2526
import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository
@@ -32,7 +33,8 @@ class AddEditTaskViewModel(application: Application) : AndroidViewModel(applicat
3233

3334
// Note, for testing and architecture purposes, it's bad practice to construct the repository
3435
// here. We'll show you how to fix this during the codelab
35-
private val tasksRepository = DefaultTasksRepository.getRepository(application)
36+
//private val tasksRepository = DefaultTasksRepository.getRepository(application)
37+
private val tasksRepository = (application as TodoApplication).taskRepository
3638

3739
// Two-way databinding, exposing MutableLiveData
3840
val title = MutableLiveData<String>()

25-to-do-notes/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class DefaultTasksRepository(
3939
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
4040
) : TasksRepository {
4141

42-
companion object {
42+
/*companion object {
4343
@Volatile
4444
private var INSTANCE: DefaultTasksRepository? = null
4545
@@ -52,7 +52,7 @@ class DefaultTasksRepository(
5252
}
5353
}
5454
}
55-
}
55+
}*/
5656

5757
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
5858
if (forceUpdate) {

25-to-do-notes/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.example.android.architecture.blueprints.todoapp.statistics
1818

1919
import android.app.Application
2020
import androidx.lifecycle.*
21+
import com.example.android.architecture.blueprints.todoapp.TodoApplication
2122
import com.example.android.architecture.blueprints.todoapp.data.Result
2223
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
2324
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
@@ -32,7 +33,8 @@ class StatisticsViewModel(application: Application) : AndroidViewModel(applicati
3233

3334
// Note, for testing and architecture purposes, it's bad practice to construct the repository
3435
// here. We'll show you how to fix this during the codelab
35-
private val tasksRepository = DefaultTasksRepository.getRepository(application)
36+
//private val tasksRepository = DefaultTasksRepository.getRepository(application)
37+
private val tasksRepository = (application as TodoApplication).taskRepository
3638

3739
private val tasks: LiveData<Result<List<Task>>> = tasksRepository.observeTasks()
3840
private val _dataLoading = MutableLiveData<Boolean>(false)

25-to-do-notes/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import androidx.navigation.fragment.findNavController
2828
import androidx.navigation.fragment.navArgs
2929
import com.example.android.architecture.blueprints.todoapp.EventObserver
3030
import com.example.android.architecture.blueprints.todoapp.R
31+
import com.example.android.architecture.blueprints.todoapp.TodoApplication
3132
import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository
3233
import com.example.android.architecture.blueprints.todoapp.databinding.TaskdetailFragBinding
3334
import com.example.android.architecture.blueprints.todoapp.tasks.DELETE_RESULT_OK
@@ -45,7 +46,8 @@ class TaskDetailFragment : Fragment() {
4546

4647
//private val viewModel by viewModels<TaskDetailViewModel>()
4748
private val viewModel by viewModels<TaskDetailViewModel> {
48-
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
49+
//TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
50+
TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
4951
}
5052

5153
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

25-to-do-notes/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import androidx.navigation.fragment.findNavController
3030
import androidx.navigation.fragment.navArgs
3131
import com.example.android.architecture.blueprints.todoapp.EventObserver
3232
import com.example.android.architecture.blueprints.todoapp.R
33+
import com.example.android.architecture.blueprints.todoapp.TodoApplication
3334
import com.example.android.architecture.blueprints.todoapp.data.Task
3435
import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository
3536
import com.example.android.architecture.blueprints.todoapp.databinding.TasksFragBinding
@@ -46,7 +47,8 @@ class TasksFragment : Fragment() {
4647

4748
//private val viewModel by viewModels<TasksViewModel>()
4849
private val viewModel by viewModels<TasksViewModel> {
49-
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
50+
//TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
51+
TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
5052
}
5153

5254
private val args: TasksFragmentArgs by navArgs()

0 commit comments

Comments
 (0)