이번 게시글에 있는 코드를 사용하기 위해서는 App 모듈 의존성에 다음 사항들을 추가해야한다.
testImplementation "org.hamcrest:hamcrest-all:1.3"
testImplementation "androidx.test:core-ktx:1.4.0"
testImplementation "org.robolectric:robolectric:4.3.1"
testImplementation "androidx.test.ext:junit-ktx:1.1.3"
testImplementation "androidx.arch.core:core-testing:2.1.0"
만약에 로컬테스트를 하는데 Application Context가 필요하다면? Activity나 Application과 같은 안드로이드 Component가 필요하다면?
AndroidX Test 라이브러리는 로컬 테스트를 할때 필요한 안드로이드 Component들을 모아놓은 라이브러리이다. 이런 경우 굳이 Instrument 테스트를 안 하고도 로컬 테스트 내에서 로직을 테스트해볼 수 있다. 즉 AndroidX Test 라이브러리는 Local test, Instrument test 모두에서 사용할 수 있는 통합 테스팅 라이브러리이다.
Instrument test에서는 InstrumentRegistry에서 applicationContext를 가져올 수 있지만 이는 실기기에서 행해지는 테스트이기에 실행속도가 느리다는 단점이 있다는 것을 지난 게시글을 통해 알 수 있다.
그에 반해 로컬 테스트에는 Robolectric을 통해서 RuntimeEnvironment.application
을 통해서 가져올 수 있다. (단 @RunWith(AndroidJUnit4::class)
를 추가해야한다는 점, 잊지말자)
LiveData를 테스트를 할 때 다음 두가지 순서대로 테스트를 세팅하면 된다
LiveData를 테스트하는 것은 결국 어떤 함수에 의해서 LiveData의 value가 onChange되거나 transform할 때 value가 observe되는지를 확인하면 된다.
class TasksViewModelTest {
// JUnit Rule
// 테스트 전에 세팅을 할 때 설정하는 규칙과 같은 것
// InstantTaskExecutorRule은 백그라운드에서 작업하는 Architecture Component들을 동일 스레드에서 작업할 수 있게 만드는 규칙
// 동기적으로, 반복적으로 테스트를 수행할 수 있게
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun addNewTask_setNewTaskEvent() {
// Given a fresh TasksViewModel
val taskViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
val observer = Observer<Event<Unit>> {}
try {
taskViewModel.newTaskEvent.observeForever(observer)
taskViewModel.addNewTask()
val value = taskViewModel.newTaskEvent.value
assertThat(value?.getContentIfNotHandled(), (not(nullValue())))
} finally {
taskViewModel.newTaskEvent.removeObserver(observer)
}
}
}
그러나 이런 방식으로 작성을 한다면 동일하게 반복되는 코드가 많기에(observeForever과 removeObserver하는 코드) 다음과 같은 Util 코드를 활용하여 생산성을 높일 수 있다.
// LiveDataTestUtils
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
@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 instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun addNewTask_setNewTaskEvent() {
val taskViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
taskViewModel.addNewTask()
val value = taskViewModel.newTaskEvent.getOrAwaitValue()
assertThat(value.getContentIfNotHandled(), not(nullValue()))
}
}
잘 봤습니다~ 👍