Skip to content
12 changes: 12 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,16 @@ dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion"

// Other dependencies
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"

// AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"

// Architecture Components core testing library (for LiveData test)
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ 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<Task>?): StatsResult {
val totalTasks = tasks!!.size
val numberOfActiveTasks = tasks.count { it.isActive }
return StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
return if (tasks == null || tasks.isEmpty()) {
StatsResult(0f, 0f)
} else {
val totalTasks = tasks.size
val numberOfActiveTasks = tasks.count { it.isActive }
StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
}
}

data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class TasksViewModel(application: Application) : AndroidViewModel(application) {
/**
* Called by the Data Binding library and the FAB's click listener.
*/
// local test 수행할 함수
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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


/**
* observeForever()로 등록된 옵저버를 해제하도록 작성된 확장 함수.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 {

// 1. 앞서 생성한 테스트 클래스를 open
// 2. 테스트 함수를 생성.
// 3. 테스트임을 나타내기 위해 함수 이름 위에 @Test annotation을 추가.
@Test
fun getActiveAndCompletedStats_완료된작업이없으면_100과_0으로_계산되는가() {

// 4. 작업 목록 만들기
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
// 5. 계산 함수인 getActiveAndCompletedStats()를 호출
val result = getActiveAndCompletedStats(tasks)

// 6. assertion을 사용하여 결과를 확인
assertThat(result.completedTasksPercent, `is`(0f))
assertThat(result.activeTasksPercent, `is`(100f))
}

@Test
fun getActiveAndCompletedStats_활성화된작업이없으면_0과_100으로_계산되는가() {
val tasks = listOf(
Task("title", "desc", isCompleted = true)
)
// When the list of tasks is computed with a completed task
val result = getActiveAndCompletedStats(tasks)

// Then the percentages are 0 and 100
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(100f))
}

@Test
fun getActiveAndCompletedStats_active와complete두경우모두_제대로계산되는가() {
// Given 3 completed tasks and 2 active tasks
val tasks = listOf(
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = false),
Task("title", "desc", isCompleted = false)
)
// When the list of tasks is computed
val result = getActiveAndCompletedStats(tasks)

// Then the result is 40-60
assertThat(result.activeTasksPercent, `is`(40f))
assertThat(result.completedTasksPercent, `is`(60f))
}

@Test
fun getActiveAndCompletedStats_null이입력되면_0을리턴하는가() {
// When there's an error loading stats
val result = getActiveAndCompletedStats(null)

// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}

@Test
fun getActiveAndCompletedStats_빈리스트가입력되면_0을리턴하는가() {
// When there are no tasks
val result = getActiveAndCompletedStats(emptyList())

// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.android.architecture.blueprints.todoapp.tasks

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.Event
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.Rule

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

// AndroidX Test 라이브러리의 annotation
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()

@Test
fun addNewTask_setsNewTaskEvent() {

// Given a fresh ViewModel
// AndroidX Test 라이브러리로부터 applicationContext를 얻어와 뷰모델을 생성한다.
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

// When adding a new task
tasksViewModel.addNewTask()

// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()

// LiveData 캐싱 이슈 방지를 위한 getContentIfNotHandled() 호출 참고.
assertThat(value.getContentIfNotHandled(), (not(nullValue())))
}

}
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@ ext {
rulesVersion = '1.0.1'
swipeRefreshLayoutVersion = '1.1.0'
timberVersion = '4.7.1'
hamcrestVersion = '1.3'
robolectricVersion = '4.4'
archTestingVersion = '2.1.0'
}