안드로이드 UnitTest에서 LiveData 사용시 오류 - Method getMainLooper in android.os.Looper not mocked.

임현주·2022년 6월 27일
0
post-thumbnail

🚨 문제

안드로이드 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로 하드코딩 되어있기 때문이지 ଘ(∩◉ω◉ )⊃----⭐️ !

사용하는 방법은 아래와 같다.

  1. 라이브러리 추가
dependencies {
	testImplementation 'androidx.arch.core:core-testing:2.1.0'
}
  1. Unit Test 코드 내에 InstantTaskExecutorRule Rule 추가
@Rule
@JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()

문제 해결 완료! 뿅!

profile
🐰 피드백은 언제나 환영합니다

0개의 댓글