Skip to content
This repository was archived by the owner on Dec 11, 2024. It is now read-only.

Create service locator #291

Open
wants to merge 27 commits into
base: starter_code
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a9612ed
TDD Unit test for StatisticsUtils
pzanzane May 25, 2023
3425ad6
Merge pull request #1 from pzanzane/UnitTestUtility
pzanzane May 25, 2023
de1a52a
Added Roboelectric and AndroidX Test suit to test android framework d…
pzanzane May 26, 2023
1d5be9c
Added Roboelectric and AndroidX Test suit to test android framework d…
pzanzane May 26, 2023
6154279
Merge branch 'viewmodel_test' of https://github.com/pzanzane/android-…
pzanzane May 26, 2023
bd52f20
Merge pull request #2 from pzanzane/viewmodel_test
pzanzane May 26, 2023
b2a035e
Test for Live Data
pzanzane May 26, 2023
3c2587d
Merge pull request #3 from pzanzane/test_livedata
pzanzane May 26, 2023
062587a
Test added for addNewTask
pzanzane May 29, 2023
9aed5b6
Merge pull request #4 from pzanzane/AllTaskTest
pzanzane May 29, 2023
548e4f6
DefaultRepository Test
pzanzane May 29, 2023
f90d83b
Merge pull request #5 from pzanzane/viewmodel_test
pzanzane May 29, 2023
7399f8a
Add fake Task Repository
pzanzane May 30, 2023
33980ca
Dependency Injection into ViewModel using ViewModelFactory
pzanzane May 30, 2023
ee24326
Merge pull request #7 from pzanzane/create_taskviewmodelfactory
pzanzane May 30, 2023
1a60143
Depdency added for Instrumented Changes
pzanzane May 30, 2023
943aeff
Merge pull request #8 from pzanzane/gradle_changes
pzanzane May 30, 2023
ff14286
Create service locator to inject into fragments
pzanzane May 31, 2023
206d4cb
Create service locator to inject into fragments
pzanzane May 31, 2023
826a629
Merge pull request #9 from pzanzane/create_service_locator
pzanzane Jun 2, 2023
d7c91d7
Add fake android default repository
Jun 2, 2023
c2de138
Add fake android default repository
Jun 2, 2023
bbcfe81
Merge branch 'fake_android_test' of https://github.com/pzanzane/andro…
Jun 4, 2023
58c23b3
Merge pull request #10 from pzanzane/fake_android_test
pzanzane Jun 4, 2023
d2bf4dc
Create service locator to inject into fragments
pzanzane May 31, 2023
e9a21bc
Create service locator to inject into fragments
pzanzane May 31, 2023
4fcae71
Merge branch 'create_service_locator' of https://github.com/pzanzane/…
pzanzane Jun 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ apply plugin: "androidx.navigation.safeargs.kotlin"
android {
compileSdkVersion rootProject.compileSdkVersion

/**
* Allows to access Android Resources in your unit tests, including AndroidManifest file.
* This will resolve "no such as manifest file: " warning.
*/
testOptions.unitTests {
includeAndroidResources = true
}

defaultConfig {
applicationId "com.example.android.architecture.blueprints.reactive"
minSdkVersion rootProject.minSdkVersion
Expand All @@ -25,6 +33,10 @@ android {
enabled = true
enabledForTests = true
}

packagingOptions {
resources.excludes.add("META-INF/*")
}
}

dependencies {
Expand Down Expand Up @@ -52,10 +64,25 @@ dependencies {

// Dependencies for local unit tests
testImplementation "junit:junit:$junitVersion"
testImplementation "com.google.truth:truth:1.1.2"

// AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

// AndroidX Test - Instrumented testing
androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
debugImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion"
debugImplementation "androidx.test.espresso:espresso-core:$espressoVersion"

// Dependencies for Android instrumented unit tests
debugImplementation "junit:junit:$junitVersion"
debugImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

debugImplementation "androidx.fragment:fragment-testing:$fragmentVersion"
debugImplementation "androidx.test:core:$androidXTestCoreVersion"

// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.example.android.architecture.blueprints.todoapp.data.source

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.Task
import kotlinx.coroutines.runBlocking

class FakeAndroidDefaultTaskRepository : IDefaultTasksRepository {

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 suspend fun updateTasksFromRemoteDataSource() {
TODO("Not yet implemented")
}

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 Result.Error -> Result.Error(tasks.exception)
is Result.Success -> {
val task = tasks.data.firstOrNull { it.id == taskId }
?: return@map Result.Error(Exception("Not found"))
Result.Success(task)
}
}
}
}

override suspend fun updateTaskFromRemoteDataSource(taskId: String) {
TODO("Not yet implemented")
}

override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
if (shouldReturnError) {
return Result.Error(Exception("Test exception"))
}
tasksServiceData[taskId]?.let {
return Result.Success(it)
}
return Result.Error(Exception("Could not find task"))
}

override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (shouldReturnError) {
return Result.Error(Exception("Test exception"))
}
return Result.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 getTaskWithId(id: String): Result<Task> {
TODO("Not yet implemented")
}

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,50 @@
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.FakeAndroidDefaultTaskRepository
import com.example.android.architecture.blueprints.todoapp.data.source.IDefaultTasksRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@ExperimentalCoroutinesApi
@MediumTest
@RunWith(AndroidJUnit4::class)
internal class TaskDetailFragmentTest {

private lateinit var repository: IDefaultTasksRepository

@Before
fun initRepository() {
repository = FakeAndroidDefaultTaskRepository()
ServiceLocator.tasksRepository = repository
}

@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}

@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest {
// Assign - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)

// Act - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

Thread.sleep(5000)
// Assert
val value = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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.IDefaultTasksRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
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: IDefaultTasksRepository? = null
@VisibleForTesting set

@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// clear all data to avoid test pollution
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}

fun provideTaskRepository(context: Context): IDefaultTasksRepository {

synchronized(this) {
return tasksRepository ?: createTaskRepository(context)
}
}

private fun createTaskRepository(context: Context): IDefaultTasksRepository {

val repository =
DefaultTasksRepository(TasksRemoteDataSource, createLocalDataSource(context))
tasksRepository = repository
return repository
}

private fun createLocalDataSource(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, /* name= */"Tasks.db"
).build()
database = result
return result
}
}
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.IDefaultTasksRepository
import timber.log.Timber
import timber.log.Timber.DebugTree

Expand All @@ -28,6 +29,9 @@ import timber.log.Timber.DebugTree
*/
class TodoApplication : Application() {

val tasksRepository: IDefaultTasksRepository
= ServiceLocator.provideTaskRepository(applicationContext)

override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
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,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).tasksRepository

// Two-way databinding, exposing MutableLiveData
val title = MutableLiveData<String>()
Expand Down
Loading