안드로이드 Test 학습 시작과 동시에 만난 오류..
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
at android.os.Looper.getMainLooper(Looper.java)
at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread(DefaultTaskExecutor.java:77)
at androidx.arch.core.executor.ArchTaskExecutor.isMainThread(ArchTaskExecutor.java:116)
at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:486)
at androidx.lifecycle.LiveData.observeForever(LiveData.java:224)
internal abstract class ViewModelTest : KoinTest {
...
protected fun <T> LiveData<T>.test(): LiveDataTestObserver<T> {
val testObserver = LiveDataTestObserver<T>()
observeForever(testObserver) // 🚨 최초 문제 발생 지점
return testObserver
}
}
LiveData를 사용하는 ViewModel 테스트를 진행할 때 생기는 문제이다.
(같은 상황이더라도 경우에 따라 NullPointerException이 뜰 때도 있다.)
최초 문제 발생 지점인 observeForever()
메소드를 살펴보자.
@MainThread
public void observeForever(@NonNull Observer<? super T> observer) {
assertMainThread("observeForever");
AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing instanceof LiveData.LifecycleBoundObserver) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
wrapper.activeStateChanged(true);
}
...
static void assertMainThread(String methodName) {
if (!ArchTaskExecutor.getInstance().isMainThread()) {
throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
+ " thread");
}
}
가장 먼저 실행되는 assertMainThread()
메소드 내부에서 isMainThread()
함수를 사용하여 현재 스레드가 메인 스레드인지를 확인하는 것을 볼 수 있다.
안드로이드 java 아래 총 3가지 항목이 있는데,
main
: 실제 앱을 구성하는 코드androidTest
: 안드로이드 디바이스를 이용한 테스트 (UI테스트)test
: 로컬 테스트 (오직 개발 장비의 JVM에서 돌아가게 되며, 디바이스가 필요 없는 테스트)우리가 테스트중인 test
환경(로컬 테스트)에서는 real Android 환경이 아니기 때문에 MainThread(=UIThread)를 사용할 수 없어 오류가 발생하는 것이다. (Android MainThread는 android.os의 Looper를 사용)
위와 같은 문제를 해결해주기 위해 사용하는 클래스가 InstantTaskExecutorRule
이다.
A JUnit Test Rule that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously.
You can use this rule for your host side tests that use Architecture Components.
아키텍처 구성 요소에서 사용하는 백그라운드 실행기를 각 작업을 동기적으로 실행하는 다른 실행기로 바꾸는 JUnit 테스트 규칙. 아키텍처 구성 요소를 사용하는 호스트 측 테스트에 이 규칙을 사용할 수 있습니다.
즉, InstantTaskExecutorRule
를 사용하면 안드로이드 구성요소 관련 작업들을 모두 동일한 스레드에서 실행시키기 때문에 동기화로 인한 고민을 할 필요가 없어진다는 것이다.
그렇더라도 MainThread를 사용하는건 아닌데 어떻게 이런 일이 가능할까 🤔 ?
public class InstantTaskExecutorRule extends TestWatcher {
@Override
protected void starting(Description description) {
super.starting(description);
ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
@Override
public void executeOnDiskIO(Runnable runnable) {
runnable.run();
}
@Override
public void postToMainThread(Runnable runnable) {
runnable.run();
}
@Override
public boolean isMainThread() {
return true;
}
});
}
...
}
내부에 isMainThread()
가 true
로 하드코딩 되어있기 때문이지 ଘ(∩◉ω◉ )⊃----⭐️ !
사용하는 방법은 아래와 같다.
dependencies {
testImplementation 'androidx.arch.core:core-testing:2.1.0'
}
@Rule
@JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
문제 해결 완료! 뿅!