Android TDD CodeLab 01. Testing Basics Task. 8~14
repository 코드에는 테스트 복잡성이 추가되는 비동기 코드, 데이터베이스 및 네트워크 호출이 포함됩니다. 지금은 이를 피하고 repository의 어떤 것도 직접 테스트하지 않는 ViewModel 기능에 대한 테스트 작성에 집중할 것입니다.
작성할 테스트는 addNewTask 메서드를 호출할 때 새 작업 창을 열기 위한 이벤트가 발생하는지 확인합니다. 테스트할 앱 코드는 다음과 같습니다.
//event란?
//Event LiveData는 TasksFragment에서 관찰됩니다.
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
TasksViewModel에서 클래스 이름을 block하고 command+N 을 눌러서 테스트 클래스를 만듭니다.
ㅤ
class TasksViewModelTest {
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh TasksViewModel
// When adding a new task
// Then the new task event is triggered
}
}
테스트할 TasksViewModel 인스턴스를 만들 때 해당 생성자에는application context가 필요합니다. 그러나 이 테스트에서는 activity, UI 및 fragment로 application을 만들지 않습니다.
그렇다면 application context를 어떻게 얻습니까?
class TasksViewModel(application : Application) : ApplicationViewModel()
AndroidX Test libraries에는 테스트를 위한 Applications and Activities과 같은 구성 요소를 제공하는 클래스와 메서드가 포함되어 있습니다.
simulated Android framework classes(예: application context)가 필요한 로컬 테스트가 있는 경우 다음 단계에 따라 AndroidX 테스트를 올바르게 설정하십시오.
1. Add the AndroidX Test core and ext dependencies
2. Add the Robolectric Testing library dependency
3. Annotate the class with the AndroidJunit4 test runner
4. Write AndroidX Test code
app/build.gradle
// AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Test code
}
// Given a fresh ViewModel
//이 시점에서 AndroidX 테스트 라이브러리를 사용할 수 있습니다.
//여기에는 애플리케이션 컨텍스트를 가져오는 ApplicationProvider.getApplicationContext 메소드가 포함됩니다.
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
//AndroidX test library에서 application context 가져옴
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
// TODO test LiveData
}
}
AndroidX Test는 테스트용 라이브러리 모음입니다. 여기에는 테스트를 위한 Applications, Activities과 같은 components을 제공하는 클래스와 메서드가 포함됩니다.
testRunner이 있어야 Junit test가 된다.
기본적으로 제공하는 testRunner에서 다른 testRunner로 바꾸는 annotation
AndroidX 테스트를 사용하는 모든 클래스에서 사용됩니다.
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
//이것은 JUnit에서 정의한 규칙이다.
// 해당 클래스에서 발생하는 모든 아키텍처 컴포넌트(LiveData)의 백그라운드 실행을 하나의 스레드에서 작동시켜준다.
// 그로 인해 thread safe 한 상태에서 테스트를 동작시킬 수 있다.
//또한 동기적으로 반복 가능한 순서로 발생하도록 해준다.
//live data 테스트 시 사용
//testImplementation "androidx.arch.core:core-testing:$archTestingVersion" dependency 추가 해야함
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Other code...}
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// Create observer - no need for it to do anything!
val observer = Observer<Event<Unit>> {}
try {
//원래는 activity나 fragment의 생명주기가 livedata를 관찰한다.
//하지만 테스트 시에는 activity나 Fragment의 생명주기를 가져 올 수 없다.
//그래서 observeForever를 사용한다.
//observer the livedata forever
// Observe the LiveData forever
tasksViewModel.newTaskEvent.observeForever(observer)
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.value
assertThat(value?.getContentIfNotHandled(), (not(nullValue())))
} finally {
// Whatever happens, don't forget to remove the observer!
tasksViewModel.newTaskEvent.removeObserver(observer)
}
}
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
//livedata 관찰을 위한 observer를 더 쉽게 만들기 위한 class
//test source set -> tasks 안에 만들기
//observe을 추가하고 LiveData 값을 가져온 다음 observer를 정리하는 getOrAwaitValue라는 Kotlin 확장 함수를 생성합니다.
@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
}
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
//observe forever 없이 LiveDataTestUtil에서 정의한 대로 livedata 값 가져옴
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
////event이기 때문에 getContentIfNotHandled로 value 가져오기
assertThat(value.getContentIfNotHandled(), not(nullValue()))
}
모든 작업을 표시하도록 필터 유형을 설정한 경우 작업 추가 버튼이 표시되는지 확인하는 테스트
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.*
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
//testRunner이 있어야 Junit test가 된다. 기본적으로 제공하는 testRunner에서 다른 testRunner로 바꾸는 annotation
//AndroidX 테스트를 사용하는 모든 클래스에서 사용됩니다.
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
//이것은 JUnit에서 정의한 규칙이다.
// 해당 클래스에서 발생하는 모든 아키텍처 컴포넌트(LiveData)의 백그라운드 실행을 하나의 스레드에서 작동시켜준다.
// 그로 인해 thread safe 한 상태에서 테스트를 동작시킬 수 있다.
//또한 동기적으로 반복 가능한 순서로 발생하도록 해준다.
//live data 테스트 시 사용
//testImplementation "androidx.arch.core:core-testing:$archTestingVersion" dependency 추가 해야함
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
private lateinit var tasksViewModel: TasksViewModel
@Before //👈여러 테스트에 대해 반복된 설정 코드가 있는 경우 @Before 주석을 사용하여 설정 방법을 만들고 반복되는 코드를 제거할 수 있습니다.
fun setUpViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh TasksViewModel
// val tasksViewModel =
// TasksViewModel(ApplicationProvider.getApplicationContext()) //AndroidX test library에서 application context 가져옴
val observer = Observer<Event<Unit>> {}
try {
//원래는 activity나 fragment의 생명주기가 livedata를 관찰한다.
//하지만 테스트 시에는 activity나 Fragment의 생명주기를 가져 올 수 없다.
//그래서 observeForever를 사용한다.
//observer the livedata forever
tasksViewModel.newTaskEvent.observeForever(observer)
// When adding a new task
tasksViewModel.addNewTask()
//then the new task event is triggered
val value = tasksViewModel.newTaskEvent.value
assertThat(value?.getContentIfNotHandled(), (not(nullValue())))
} finally {
// Whatever happens, don't forget to remove the observer!
tasksViewModel.newTaskEvent.removeObserver(observer)
}
}
@Test
fun addNewTask_setsNewTaskEvent_with_util() {
// Given a fresh TasksViewModel
// val tasksViewModel =
// TasksViewModel(ApplicationProvider.getApplicationContext()) //AndroidX test library에서 application context 가져옴
tasksViewModel.addNewTask()
val value =
tasksViewModel.newTaskEvent.getOrAwaitValue() //observe forever 없이 LiveDataTestUtil에서 정의한 대로 livedata 값 가져옴
assertThat(
value.getContentIfNotHandled(), not(nullValue())
) //event이기 때문에 getContentIfNotHandled로 value 가져오기
}
@Test //👈 테스트 코드 !!!
fun setFilterAllTasks_tasksAddViewVisible() {
// Given a fresh ViewModel
// val taskViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When the filter type is ALL_TASKS
tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
// Then the "Add task" action is visible
val value = tasksViewModel.tasksAddViewVisible.getOrAwaitValue()
assertThat(value, `is`(true))
}
}