안드로이드에서 테스트하기 - LiveData(AndroidX) Test

이현우·2021년 8월 2일
4

Deep Dive: Android

목록 보기
8/10
post-thumbnail
post-custom-banner

이번 게시글에 있는 코드를 사용하기 위해서는 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"

AndroidX Test Basics

Why use this?

만약에 로컬테스트를 하는데 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)를 추가해야한다는 점, 잊지말자)

Testing LiveData

LiveData를 테스트를 할 때 다음 두가지 순서대로 테스트를 세팅하면 된다

  • InstantTaskExecutorRule
  • LiveData를 Observe한다.

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()))
    }
}
profile
이현우의 개발 브이로그
post-custom-banner

3개의 댓글

comment-user-thumbnail
2021년 8월 5일

잘 봤습니다~ 👍

1개의 답글

이게 현우쓰의 바이브..? 그저 빛

답글 달기