Skip to content

Commit

Permalink
add testing fragment and ServiceLocator
Browse files Browse the repository at this point in the history
  • Loading branch information
solygambas committed Apr 22, 2022
1 parent d8d4919 commit aa7e022
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.example.android.architecture.blueprints.todoapp.data.source

import androidx.annotation.VisibleForTesting
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 kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap


class FakeAndroidTestRepository : TasksRepository {

var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

private var shouldReturnError = false

private val observableTasks = MutableLiveData<Result<List<Task>>>()

fun setReturnError(value: Boolean) {
shouldReturnError = value
}

override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}

override suspend fun refreshTask(taskId: String) {
refreshTasks()
}

override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}

override fun observeTask(taskId: String): LiveData<Result<Task>> {
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<Task> {
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<List<Task>> {
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<String, Task>
}

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() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.android.architecture.blueprints.todoapp.taskdetail

import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.FakeAndroidTestRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Assert.*
import org.junit.Before

import org.junit.Test
import org.junit.runner.RunWith

@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", "Android rocks", false)
repository.saveTask(activeTask)

// When - details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.example.android.architecture.blueprints.todoapp

import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.room.Room
import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository
import com.example.android.architecture.blueprints.todoapp.data.source.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 var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
private val lock = Any()

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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.example.android.architecture.blueprints.todoapp

import android.app.Application
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import timber.log.Timber
import timber.log.Timber.DebugTree

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

override fun onCreate() {
super.onCreate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,7 +33,8 @@ 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 = DefaultTasksRepository.getRepository(application)
private val tasksRepository = (application as TodoApplication).taskRepository

// Two-way databinding, exposing MutableLiveData
val title = MutableLiveData<String>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class DefaultTasksRepository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksRepository {

companion object {
/*companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
Expand All @@ -52,7 +52,7 @@ class DefaultTasksRepository(
}
}
}
}
}*/

override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (forceUpdate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,7 +33,8 @@ 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 = DefaultTasksRepository.getRepository(application)
private val tasksRepository = (application as TodoApplication).taskRepository

private val tasks: LiveData<Result<List<Task>>> = tasksRepository.observeTasks()
private val _dataLoading = MutableLiveData<Boolean>(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,7 +46,8 @@ class TaskDetailFragment : Fragment() {

//private val viewModel by viewModels<TaskDetailViewModel>()
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
//TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,7 +47,8 @@ class TasksFragment : Fragment() {

//private val viewModel by viewModels<TasksViewModel>()
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
//TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}

private val args: TasksFragmentArgs by navArgs()
Expand Down

0 comments on commit aa7e022

Please sign in to comment.