본문 바로가기

Diary/삽질노트

[Kotlin Coroutine] ViewModelScope에 delay가 포함된 Unit test 삽질, Delay controller

 

22.02.23
최근에 릴리즈된 1.6.0 버전에서 이 포스팅에서 사용했던 TestCoroutineDispatcher 가 Deprecated 되어서 업데이트함

---

 

class ViewModel {
    fun loadData() {
        viewModelScope.launch {
            delay(5000L) // 여기를 집중
            _state.value = doSomething() // suspend function
        }
    }
}

위와 같은 뷰모델과

@Test
fun `test loadData()`() = runBlockingTest {
    // given ..
    
    viewModel.loadData()
    
    assertEquals(expected, viewModel.state.data)
}

테스트코드를 작성하고 수행하면 null-exception 이 발생한다.

당연하게도 viewModelScope 에서 시작된 doSomething() 메서드가 시작되기도 전에, 테스트 함수의 assertEquals 코드라인이 수행되어버린 것이다.

 

그래서 위와 같은 타이밍 이슈를 해결하기 위한 여러가지 방법들이 있다. 대충 암거나 찾아본 것

Main dispatcher 설정 시 StandardTestDispatcher 로 되도록 하고, 테스트 Rule 에 추가해주는 방식으로 테스트에 사용되는 Dispatcher 와 뷰모델에 설정되는 dispatcher 를 동일하게 등록해서 타이밍 이슈를 해결할 수 있다.

Dispatchers.setMain(dispatcher)

 

다만 위와 같이 설정하더라도, viewModelScope 안에서 delay() 메서드를 수행하면 타이밍 이슈가 해결되지 않는다.

delay() 메서드를 건너뛰고, viewModelScope 내의 loadData() 메서드가 수행되기 전에 테스트 함수가 종료되어버린다. 테스트 함수의 Scope, 뷰모델의 Scope 가 달라서일까?

 

TestCoroutineDispatcher 를 살펴보면 아래와 같은 주석이 달려있다. (TestCoroutineDispatcher deprecated -> StandardTestDispatcher.scheduler 참고)

CoroutineDispatcher that performs both immediate and lazy execution of coroutines in tests and implements DelayController to control its virtual clock.

By default, TestCoroutineDispatcher is immediate. That means any tasks scheduled to be run without delay are immediately executed. If they were scheduled with a delay, the virtual clock-time must be advanced via one of the methods on DelayController.

When switched to lazy execution using pauseDispatcher any coroutines started via launch or async will not execute until a call to DelayController.runCurrent or the virtual clock-time has been advanced via one of the methods on DelayController.

 

완벽하게 이해하기는 좀 어렵지만 중요한 부분만 살펴보면, TestCoroutineDispatcher 는 기본적으로 즉시 실행되며 delay() 를 사용한 경우 DelayController 의 메서드를 이용해 가상 clock-time 을 앞당겨야한다고 나와있다. TestCoroutineDispatcher 는 DelayController 를 구현하고있으며, 그 말인 즉슨 dispatcher 를 이용해 clock-time 을 직접 조정해야한다는 것이다.

 

그래서 StandardTestDispatcher.scheduler.advanceUntilIdle() 를 사용한 그 순간에 delay() 된 모든 시간만큼 clock-time 을 진행시켜 delay() 함수를 끝내버릴 수 있게 된다. 미래로 시간을 강제로 돌려서 어거지로 타이밍을 맞추는 느낌이다.

 

그래서 아래와 같이 코드를 추가해주면 테스트는 정상적으로 수행된다.

@Test
fun `test loadData()`() = runBlockingTest {
    // given ..
    
    viewModel.loadData()
    
    // 메서드명만 보면 Idle 상태가 될 때까지 기다리겠다! 라는 느낌이 강하게 든다. 실제 동작도 그렇게 되는 것 같은데..
    dispatcher.scheduler.advancedUntilIdle()
    
    assertEquals(expected, viewModel.state.data)
}

 

사실 viewModelScope 안에서 delay() 만 안넣어줬으면 내가 작성한 테스트케이스는 모두 통과한다. 넣을 필요도 없다..

근데 왜 통과하지? 그럴리가 없는데? delay() 를 넣어볼까? 하면서 넣었다가 이렇게 큰 코 다치게 되었다. 몇 시간동안 삽질하면서 delay() 를 잘못 사용한 것 같다는 느낌이 강하게 들었지만, 오기가 생겨서 놓을 수가 없었다...ㅠㅠ

내가 구글링을 잘 못 한건진 몰라도, 타이밍 이슈 해결을 위해 advanceUntilIdle() 를 사용하라는 답변을 본 적이 없어서 이렇게 포스팅까지 하게됐다! (사실 TestCoroutineDispatcher 주석만 봤으면 진작에 해결했을텐데 말이다,,,,,,,)

 

delay()가 추가된 코루틴에 대한 테스트 코드를 수행하기 위해서는 DelayController 를 써먹자!!