diff --git a/app/build.gradle b/app/build.gradle index edea693..580ca6b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" + } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt index 968b8f1..7ffd78d 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt @@ -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?): 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) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt index 8cc67bb..e600c12 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt @@ -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) } diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/LiveDataTestUtil.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/LiveDataTestUtil.kt new file mode 100644 index 0000000..e3eaa18 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/LiveDataTestUtil.kt @@ -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 LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS, + afterObserve: () -> Unit = {} +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + 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 +} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt new file mode 100644 index 0000000..2668893 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt @@ -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("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)) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt new file mode 100644 index 0000000..22226db --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt @@ -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()))) + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index c851a43..7495e05 100644 --- a/build.gradle +++ b/build.gradle @@ -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' }