테스트는 메인(main) 스레드가 아닌 다른 스레드에서 실행되기 때문에 실패하게 된다.
다시 돌아와서 LiveData의 setValue의 내부를 보면 assertMainThread라는 메소드를 호출하고 있고 assertMainThread는 ArchTaskExecutor의 isMainThread 메소드를 이용해서 메인 스레드가 아니면 예외(Exception)를 발생시키는 것을 볼 수 있다.
마찬가지로 postValue도 메인 루퍼를 이용해서 메인 스레드로 값을 보내고 있기 때문에, setValue와 postValue 둘 다 메인 스레드가 필요하게 된다.
기본적으로 제공하는 룰 중에 InstantTaskExecutorRule이라는 게 있다. 이 룰의 내부를 보면 setValue에서 보았던 ArchTaskExecutor가 새로운 TaskExecutor를 생성하면서 isMainThread 메소드를 강제로 true로 리턴하고 있다.
viewModelScope.launch는 기본적으로 다른 Dispatcher가 적용되지 않으면 메인 스레드를 사용한다. 그래서 LiveData 때와 마찬가지로 테스트는 다른 스레드에서 실행되기 때문에 실패하게 된다.
먼저 testDispatcher를 하나 만들어 준다. @Before에서 메인을 사용하는 Dispatcher를 testDispatcher로 주입하여 변경하는 작업을 하고, 테스트가 끝난 후 @After에서는 testDispatcher로 변경한 작업을 해제해 주고 있다.
private val testDispatcher = TestCoroutineDispatcher()
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tesrDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
testDispatcher 작업을 하기 전 흐름도는, setMain을 이용해서 testDispatcher 를 주입한 후 다음과 같이 변한다.

모든 테스트에 @Before와 @After를 만들면 귀찮으니 룰을 하나 만들어 보겠다.
MainCoroutineRule 클래스를 생성하고 앞에서 본 InstantTaskExecutorRule과 동일하게 TestWatcher를 상속받는다.
starting에서 메인(main) 스레드를 사용하는 Dispatcher를 testDispatcher로 변경해 주고, finished도 마찬가지로 resetMain을 똑같이 해 주면 룰이 완성된다.
https://tech.kakao.com/posts/479
테스트는 MainThread에서 수행되지 않고, WorkerThread에서 작동한다.
viewModelScope.launch는 Dispatcher가 적용되지 않으면 MainThread에서 작동한다.
테스트는 Test worker 스레드에서 동작하고, Dispatcher가 적용되지 않은 viewModelScope.launch는 Main 스레드에서 작업이 일어나야한다. 이 과정에서 오류가 발생한다.
스레드가 달라서 발생하는 문제를 해결하기 위해 메인을 사용하는 Dispatcher가 TestDispatcher를 사용하도록 변경한다. 즉, 테스트가 일어나는 스레드를 통일하다.
테스트가 종료된 후에는 다시 초기 상태처럼 메인 스레드를 사용할 수 있도록 Dispatcher의 Main을 초기화한다.
@Before
@OptIn(ExperimentalCoroutinesApi::class)
fun setUp() {
Dispatchers.setMain(UnconfinedTestDispatcher()) // Main Dispatcher 변경
}
@After
@OptIn(ExperimentalCoroutinesApi::class)
fun tearDown() {
Dispatchers.resetMain() // Main Dispatcher를 초기 상태로 복구
}
앞서 설명한 대로 테스트가 시작할 때 스레드를 설정하고, 테스트가 끝날 때 스레드를 다시 설정하는 코드가 있으면 테스트가 조금 더 귀찮아질 것이다. 그러니 룰을 생성해 이 문제를 해결하도록 하겠다.
아래의 코드처럼 룰을 작성해 준다.
만들어 둔 룰을 테스트 룰에 적용해 준다면 테스트 시작과 종료 시점에 스레드를 설정하는 코드를 추가로 작성하지 않아도 된다.
@OptIn(ExperimentalCoroutinesApi::class)
class MainCoroutineRule constructor(
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
@OptIn(ExperimentalCoroutinesApi::class)
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
https://korean-otter.tistory.com/234
테스트를 작성하다보면 여러 테스트 클래스에서 테스트 사전 작업, 직후 작업이 동일할 때가 있다. 즉 코루틴을 사용하는 대부분의 테스트 클래스에서는 @Before에서 Main Dispatcher를 바꾸고, @After에서 되돌려 놓는다. 이런 작업을 하나의 Rule로 만들어 놓으면 @Before, @After마다 자동으로 수행되어 보일러 플레이트 코드를 줄일 수 있다. @get:Rule annotation을 붙여 사용한다.
코드 블럭을 특별한 coroutine context에서 실행하여 동기적으로 즉각 실행한다.
delay() 함수가 사용된다면, 실제 delay 시간 만큼 기다리지 않고 테스트를 실행한다. 즉 모든 pending task들을 즉각 실행하며 가상의 clock-time을 딜레이된 시간으로 조정한다.
즉 테스트 코드에서는 대부분 실제로 delay만큼 기다릴 필요가 없기 떄문에 runBlockingTest() 함수를 사용하여 테스트 코드 시간을 단축 시킬 수 있다.
https://wooooooak.github.io/android/2020/12/04/android-testing/
이 빌더는 이전의 runBlockingTest를 대체하며, 모든 플랫폼에서 코루틴 코드를 테스트하는데 사용할 수 있다.
@Test
// fun test() = coroutineRule.runBlockingTest {
// // Test
// }
fun test() = runTest {
// Test
}
테스트 목적으로 사용하는 CoroutineDispatcher 구현이다.
새 코루틴의 실행을 예측할 수 있도록 테스트 중에 새 코루틴을 만드는 경우 TestDispatchers를 사용해야 한다.
TestDispatcher는 TestCoroutineScheduler에 의해 딜레이가 제어되는 CoroutineDispatcher다.
아래의 2가지로 나뉜다.
특별한 동작이 없는 단순한 디스패처다. 이 디스패처는 자체적으로 작업을 실행하지 않고 항상 스케줄러에게 작업을 전달한다. 실제로 이는 launch 또는 async 블록이 즉시 시작되지 않음을 의미하며 runTest 내부에서 TestScope 혹은 스케줄러가 제공하는 함수를 통해 시작되도록 제어할 수 있다.
Dispatcher.Unconfined 처럼 작동하는 디스패처다. 코루틴이 시작되는 순서를 보장하지 않지만, 코루틴이 즉각적으로 시작되기 때문에 테스트 코드에서 runCurrent나 advanceUntilIdle 같은 함수를 수동으로 호출할 필요가 없다.
StandardTestDispatcher vs UnconfinedTestDispatcher
Standard: 실행 순서에 대한 완전한 제어 가능, 코루틴이 자동으로 실행되지 않음
Unconfined : 실행 순서에 대한 완전한 제어 불가능, 코루틴이 자동으로 실행됨
https://medium.com/hongbeomi-dev/
https://developer.android.com/kotlin/coroutines/test
ViewModel과 코루틴의 생명주기를 연결하여, 자동으로 코루틴을 취소하는 기능을 제공한다.
테스트 환경에서 코루틴의 실행을 제어하기 위한 도구다.
launch나 async로 시작한 코루틴을 바로 실행하지 않고, 실행 시점을 개발자가 제어할 수 있다.
delay()와 같은 시간이 소요되는 작업을 실제 시간 대신 가상의 시간으로 빠르게 실행할 수 있다.
Dispatchers.Unconfined는 UI 디스패처(Dispatchers.Main) 대신 사용할 수 있는 것처럼 보일 수 있지만, 테스트에서 사용하면 타이밍 문제가 발생한다.
실행 순서를 보장하지 않으므로, Dispatchers.Main 대신 사용하는 것은 잘못된 접근이다.
Dispatchers.setMain(dispatcher)는 테스트 환경에서 Dispatchers.Main을 대체한다.
만약 복원하지 않으면, 다음 테스트에서도 동일한 대체 디스패처가 적용되어 부작용이 생긴다.
https://medium.com/androiddevelopers/
etc
https://medium.com/degoo/android-unit-testing-with-coroutines
MainCoroutineScopeRule 코드 참고
https://github.com/android/codelab-kotlin-coroutines/
https://medium.com/androiddevelopers/migrating-to-the-new-coroutines
https://github.com/android/architecture-components-samples
https://github.com/android/codelab-kotlin-coroutines/blob/master/coroutines
viewModelScope.launch는 기본적으로 메인 스레드을 사용하기 때문에, 테스트 환경에서 실행 시 실패한다. 이를 해결하려면 testDispatcher를 만들어 Dispatchers.Main을 대체해야 한다.