LiveData와 관련된 무해해 보이는 유닛 테스트가 왜 실패했는지 고민하거나, 무언가가 표시되어야 할 빈 화면을 멍하니 바라보게 될 때, 관찰되지 않은 LiveData는 업데이트를 전파하지 않는다는 점을 기억해야 한다.
이 문제는 특히 Transformations를 다룰 때 중요하다. 만약 LiveData가 변환된다면, 변환의 결과로 생성된 LiveData도 반드시 관찰되어야 한다. 그렇지 않으면 변환이 절대 평가되지 않는다.
"만약 LiveData가 숲에서 떨어졌는데 아무도 그것을 관찰하지 않았다면… 그건 과연 업데이트된 걸까요?"
예를 들어, 초기 _liveData1 값이 변경될 때마다 이 값이 대문자로 변환(매핑)되는 상황을 살펴보자.
class MyViewModel : ViewModel() {
private val _liveData1 = MutableLiveData<String>()
val liveData1: LiveData<String> = _liveData1
// _liveData1의 값을 대문자로 변환한 새로운 LiveData 생성
// liveData2는 _liveData1의 값을 관찰하며, 값이 변경될 때마다 변환된 결과 제공
// _liveData1의 값이 "foo"로 변경되면, liveData2는 "FOO"를 제공
val liveData2 = _liveData1.map { it.toUpperCase() }
// _liveData1의 값을 newValue로 설정
// 값을 설정하면 _liveData1의 관찰자가 알림을 받게 되고, 연결된 liveData2도 갱신
fun setNewValue(newValue: String) {
_liveData1.value = newValue
}
}
이 코드에서 liveData1은 MutableLiveData로 정의되며, setNewValue 메서드를 호출하여 값을 설정할 수 있다. 또한, liveData2는 liveData1을 기반으로 Transformations.map을 사용하여 값을 대문자로 변환한다.
// 간단한 테스트 작성
fun isLiveDataEmitting() {
viewModel.setNewValue("foo")
// 단순한 데이터 접근이기 때문에 Pass
assertEquals(viewModel.liveData1.value, "foo") // Passes
// _liveData1을 기반으로 map을 통해 변환된 결과를 반환해야 함
// 관찰되지 않았기 때문에 변환이 실행되지 않아 Fail
assertEquals(viewModel.liveData2.value, "FOO") // Fails!
}
이 테스트가 실패하는 이유는 LiveData는 불필요한 작업을 수행하지 않기 때문이다. liveData2.value를 읽는 것만으로는 의존적인 변환 체인이 활성화되지 않는다. 오직 observe()를 통해 구독(subscribe)을 해야 변환 작업이 수행된다.

Transformation의 결과는 observe되지 않으면 계산되지 않는다.
참고로, liveData1의 값은 MutableLiveData이기 때문에 변환 작업 없이도 읽을 수 있다.
이 문제를 해결하는 간단한 (하지만 최고는 아닌) 방법은 읽어야 하는 LiveDatas에서 observeForever()를 호출하는 것이다.
@Test
fun isLiveDataEmitting_observeForever() {
viewModel.setNewValue("foo")
// liveData2를 강제로 observer 하도록 설정
// liveData2의 변환 작업이 실행되고, 값이 "FOO"로 설정
viewModel.liveData2.observeForever { }
// 단순한 MutableLiveData이므로 값 "foo" 바로 반환
assertEquals(viewModel.liveData1.value, "foo")
// observeForever()를 통해 liveData2가 관찰되었기 때문에
// 변환 작업이 실행되고, 변환된 값 "FOO" 반환
assertEquals(viewModel.liveData2.value, "FOO")
}

하지만 여러 개의 LiveData를 관찰해야 한다면, 테스트가 중요하지 않은 구문으로 빠르게 채워질 수 있다.
Jetpack에서 이런 문제를 해결하기 위한 기본 제공 함수는 없지만, 직접 만들어 사용할 수 있다.
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2, // 대기할 시간
timeUnit: TimeUnit = TimeUnit.SECONDS // 시간 단위
): T {
var data: T? = null // 결과 값 저장할 변수
val latch = CountDownLatch(1) // 값이 설정될 때까지 기다리는 도구
val observer = object : Observer<T> {
// LiveData가 값을 변경할 때 실행
override fun onChanged(o: T?) {
data = o // 값 저장
latch.countDown() // 대기 종료
this@getOrAwaitValue.removeObserver(this) // 관찰자 제거
}
}
this.observeForever(observer) // LiveData에 관찰자 등록
// 값이 설정될 때까지 대기
if (!latch.await(time, timeUnit)) {
// 시간이 지나도 값이 설정되지 않으면 throw
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T // 값 return
}
getOrAwaitValue는 일시적으로 observer를 등록하고,
LiveData가 새로운 값을 받을 때까지 기다린다.
값이 설정되면 관찰자를 제거하고 그 값을 반환한다.
값이 설정되지 않으면 일정 시간 후 테스트를 멈추고 에러를 던진다.
이 함수는 LiveData를 새로운 값(onChanged를 통해)을 받을 때까지 관찰한 다음 관찰자를 제거한다. 즉, LiveData가 값을 업데이트할 때까지 잠시 기다렸다가, 값이 설정되면 그 값을 반환한다.
LiveData에 이미 값이 있는 경우 즉시 반환한다.
또한 값이 설정되지 않은 경우 2초(또는 설정한 값) 후에 예외를 throw한다. 이렇게 하면 무언가 잘못되었을 때 결코 끝나지 않는 테스트를 방지할 수 있다.
getOrAwaitValue 함수를 사용하면 LiveData가 값을 업데이트할 때까지 기다릴 수 있으며, 테스트에서 복잡하게 관찰자를 등록하거나 관리하지 않아도 된다. 값이 설정되지 않으면 테스트를 멈추고 에러를 던져 무한 대기를 방지한다.
getOrAwaitValue는 값을 하나 받으면 관찰을 멈추고 값을 반환한다.
하지만, 테스트 중에 여러 번 값이 업데이트될 가능성이 있다면 getOrAwaitValue는 적합하지 않을 수 있다.
만약 LiveData가 테스트 중 여러 값을 받을 가능성이 있다면, Jetpack의 observeForTesting을 사용하는 것이 적합하다. observeForTesting은 테스트 중 특정 시간 동안 LiveData를 관찰하고 여러 값을 확인하는 데 적합하다.
이제 테스트를 훨씬 더 읽기 쉽게 읽을 수 있다.
@Test
fun isLiveDataEmitting_getOrAwaitValue() {
viewModel.setNewValue("foo")
// Pass:
assertEquals(viewModel.liveData1.getOrAwaitValue(), "foo")
// getOrAwaitValue는 내부적으로 관찰자를 등록해
// liveData2의 변환 작업이 실행되도록 도와줌
assertEquals(viewModel.liveData2.getOrAwaitValue(), "FOO")
}
MutableLiveDatas를 사용하면 이 작업을 수행할 필요가 없지만 나중에 구현 세부 정보가 변경될 경우를 대비해 LiveData 값을 읽을 때 getOrAwaitValue를 사용하는 것이 좋다. MutableLiveDatas를 관찰하지 않고도 값을 얻을 수는 있지만 좋은 관행이다.

이 기술은 스레드 문제를 가질 수 있다는 점을 아는 것이 중요하다.
대부분의 문제는 유닛 테스트에 InstantTaskExecutorRule을 추가함으로써 해결할 수 있다.
하지만 메인 스레드에서 LiveData.postValue()를 호출하면, 문서화된 우선순위(precendence)가 유지되지 않을 수 있다.
이것은 흔한 경우는 아니지만, 언급할 가치가 있는 이유는 이로 인해 찾아내기 어려운(evasive) 버그를 생성할 수 있기 때문이다.
만약 여러 프래그먼트에서 공유된 ViewModel을 사용하고 있다면, 모든 화면에서 동일한 인스턴스를 사용하고 있는지 확인해야한다.
프래그먼트를 ViewModelProviders에 전달하면서 LifecycleOwner로 Activity가 아닌 Fragment를 전달하거나, by ViewModels 대신 by activityViewModels()를 사용하는 경우, 이러한 문제가 발생할 수 있다.
LiveData를 반환하는 쿼리가 왜 업데이트를 전파하지 않을까?
만약 LiveData가 관찰되고 있다는 것을 확신한다면, 다른 데이터베이스 인스턴스를 사용하고 있을 가능성이 있다.
데이터베이스 생성 방식이나 DI(Dependency Injection) 그래프를 확인해야 한다.
만약 상황을 충분히 이해하고 있다면, 데이터베이스 빌더에서 다중 인스턴스 무효화 기능(multi-instance invalidation)을 활성화할 수도 있다.
하지만 이 기능에는 몇 가지 제한이 있다.
https://velog.io/@ganghee/LiveData
JUnit4에서 Rule을 설정해야 한다.
// build.gradle에 디펜던시 추가
testImplementation 'androidx.arch.core:core-testing:2.1.0
// 테스트 클래스에 Rule 추가
class CalculatorViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
...
}
라이브 데이터를 테스트하기 위해서는 다음과 같은 확장 함수 getOrAwaitValue를 추가해야 한다.
https://velog.io/@l2hyunwoo/LiveData-Test
LiveData를 테스트를 할 때 다음 두가지 순서대로 테스트를 세팅하면 된다
LiveData를 테스트하는 것은 결국 어떤 함수에 의해서 LiveData의 value가 onChange되거나 transform할 때 value가 observe되는지를 확인하면 된다.
InstantTaskExecutorRule은 백그라운드에서 작업하는 Architecture Component들을 동일 스레드에서 작업할 수 있게 만드는 규칙이다.
UnitTest를 진행하다보면 Livedata에 들어있는 값을 테스트해야 하는 경우가 생긴다 LiveData에 관한 결과를 받으려면 옵저빙을 하고 있어야한다. 이러한 역할을 getOrAwaitValue라는 함수를 이용하여 대신한다
https://jinudmjournal.tistory.com/272
LiveData는 기본적으로 백그라운드 스레드에서 동작한다.
InstantTaskExecutorRule를 활용하면 이를 테스트 환경에서 제어 가능하게 하며, 스레드 문제를 회피할 수 있다.
InstantTaskExecutorRule은 JVM 테스트 환경에서 Android Main Thread를 사용할 수 있도록 설정해준다.
testImplementation("androidx.arch.core:core-testing:2.2.0")
class MainViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
@Test
fun testLiveData() {
// Given
// LiveData 생성
val liveData = MutableLiveData<String>()
val observer = Observer<String> {}
// When
liveData.observeForever(observer)
liveData.value = "Hello, World!"
// Then
assertEquals("Hello, World!", liveData.value)
liveData.removeObserver(observer)
}
}
** JUnit5에서는 JUnit 4의 Rule 을 사용할 수 없으므로, 직접 제작해야 한다.
getOrAwaitValue는 LiveData 값을 비동기적으로 기다렸다가 테스트할 때 사용된다
getOrAwaitValue 확장 함수를 통해 LiveData 값을 관찰하고, 그 값이 설정될 때까지 기다릴 수 있다.
class LiveDataTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun testLiveData_withGetOrAwaitValue() {
val liveData = MutableLiveData<String>()
liveData.value = "Hello, World!"
val value = liveData.getOrAwaitValue()
assertEquals("Hello, World!", value)
}
}
Robolectric를 활용하면 LiveData의 생명주기를 쉽게 시뮬레이션할 수 있다.
실제 LiveData가 생명주기에 따라 어떻게 동작하는지 확인이 가능하다
LiveData인 newTaskEvent를 테스팅하기 전에 JUnit Rule을 추가한다.
(JUnit Rule은 test 전 / 후에 정의된 코드들이 실행되도록 만든다.)
여기서는 InstantTaskExecutorRule를 사용한다.
InstantTaskExecutorRule을 이용하면 안드로이드 구성요소 관련 작업들을 모두 한 스레드에서 실행되게 한다.
그러므로 모든 작업이 synchronous하게 동작하여 테스팅을 원활하게 할 수 있다. 즉 동기화 때문에 고민할 필요가 없어진다.
특히 LiveData를 이용한다면 필수적으로 InstantTaskExecutorRule를 사용해야할 것이다.
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
}
위와 같이 dependencies를 추가하고, 테스트 코드에 적용하자.
LiveData를 사용하는 경우 99%의 경우 다음 두 가지가 필요하다.
LifeCycle awareness를 위한 lifecycleOwner
onChanged Action을 위한 Observer
하지만, 우리가 현재 하려는 테스트에 Activity나 Fragment는 없다.
그러므로 여기서는 LiveData를 lifecycle과 무관하게 쓸 수 있는 observeForever()를 사용한다.
observeForever()를 사용하기 때문에 lifecycle과 무관하게 옵저버 패턴을 사용할 수 있다.
사실 위 코드는 LiveData를 테스팅할 때 마다 등록 / 취소를 해야해서 보일러플레이트 코드가 생긴다.
그래서 LiveData<T>.getOrAwaitValue를 사용한다.
https://jeroenmols.com/blog/2019/01/17/livedatajunit5/
JUnit 테스트에서 livedata.setValue() 또는 livedata.value =를 호출할 경우, 그리고 만약 unitTests.returnDefaultValues = true가 활성화되어 있다면 예외가 발생한다.
이는 JVM 유닛 테스트가 Android의 메인 스레드 개념에 대해 전혀 모르기 때문이다. JVM 테스트는 모든 테스트를 임의의(백그라운드) 스레드에서 실행한다.
참고: 메인 스레드는 UI 프레임워크(예: Android, Swing 등)에서 제공하는 개념으로, UI 업데이트 시 경쟁 상태(race conditions)를 방지하기 위해 존재합니다.
postValue()를 호출할 경우에도 유사한 문제가 발생한다.
테스트 중에 메인 스레드가 존재하더라도, 값 업데이트가 비동기로 발생하므로, 값이 업데이트되기 전에 assert가 실행될 수 있다.
InstantTaskExecutorRule를 통해 위의 문제점을 해결할 수 있다.
즉, 메인 스레드가 없어도 LiveData 업데이트가 즉시 이루어지도록 강제하고, postValue()로 인해 발생하는 비동기 문제를 해결한다
LiveData 작업을 동기적으로 처리, 비동기 작업을 피함
https://sh1mj1-log.tistory.com/175
LiveData의 유닛 테스트를 위해서는 비동기 작업을 동기적으로 처리할 수 있도록 InstantTaskExecutorRule을 설정하는 것이 필수다. 이 Rule은 테스트 환경에서 LiveData가 즉시 값을 업데이트하도록 강제하여, 비동기로 인한 예기치 않은 동작을 방지한다.
또한, LiveData 값을 테스트하려면 강제 관찰을 위한 observeForever를 사용할 수 있지만, 테스트가 끝난 후 반드시 관찰자를 제거해야 한다. 이를 대체하기 위해, getOrAwaitValue와 같은 확장 함수를 사용하면 값 업데이트를 기다리는 동안 자동으로 관찰자를 관리할 수 있다. 그 외에 observeForTesting도 있다.