Coroutine/Flow Test
코루틴 1.6.0의 테스트
코루틴 1.6.0 이 릴리즈 되면서 kotlinx-coroutines-test 패키지가 변경되었다. 1.5.2 와 비교하여 대표적인 변경 내역은 다음과 같다.
TestCoroutineDispatcher
(Deprecated) →StandardTestDispatcher
,UnconfindTestDispatcher
TestCoroutineScope
(Deprecated) →TestScope
runBlockingTest
(Deprecated) →runTest
이 글에서는 코루틴 문서를 바탕으로 변경되거나 새로 추가된 클래스들의 사용 방법과 동작 과정에 대해 소개한다.
개요
테스트 패키지에서 제공하는 유틸리티
runTest
: 테스트 블록 내에서 delay 호출 시 발생하는 지연을 자체적으로 생략하고, uncaught exception을 처리한다.TestCoroutineScheduler
: virtual time을 위해 공유되는 소스. 코루틴 실행 순서와 딜레이 생략의 제어를 위해 사용된다. 여러 개의 디스패처에서 동일한 TestCoroutineScheduler를 공유할 수 있다. 이 디스패처를 사용하는 코루틴은 TestCoroutineScheduler의 virtual time에 따라 동기화된다.TestScope
: runTest와 함께 사용되는 코루틴 스코프로, TestCoroutineScheduler에 대한 접근을 제공한다.TestDispatcher
: TestCoroutineScheduler에 의해 딜레이가 제어되는 CoroutineDispatcherDispatchers.setMain
: argument로 전달한 CoroutineDispatcher로 main dispatcher를 모킹하는 함수.
테스트 패키지에서 제공하는 TestDispatcher 구현
StandardTestDispatcher
: TestCoroutineScheduler와 연결되는 것 외에는 특별한 기능을 제공하지 않는 디스패처.UnconfinedTestDispatcher
: Dispatchers.Unconfined 처럼 동작하는 디스패처.
TestScope에 디스패처를 지정하지 않는다면 기본적으로 StandardTestDispatcher가 등록된다.
StandardTestDispatcher
를 사용하면 테스트 블록 내에서 생성된 코루틴을 즉시 실행하지 않고, 일반적인 코루틴 처럼 suspend 함수가 호출될 때 스케줄링된 순서대로 실행한다. TestScope
는 코루틴 실행 순서를 제어하는 함수를 제공한다.
UnconfinedTestDispatcher
를 테스트 블록 내에서 생성된 코루틴을 즉시 실행한다. 하지만 이는 runTest 블록의 top-level 에서만 동작하며, child 코루틴에서 launch 또는 async를 호출한다면 코루틴 실행 순서가 보장되지 않는다.
테스트 클래스 형식
Before
After
Dispatchers.setMain
호출로 main dispatcher가 모킹되었기 때문에 Dispatchers.Main을 통해 TestDispatcher
에 대해 접근할 수 있으며 runTest 함수의 테스트 블록 내에서 this로 TestScope
에 접근할 수 있다.
scheduler의 직접적인 조작이 필요하다면 TestScope에서 testScheduler 프로퍼티로 TestCoroutineScheduler
를 참조할 수 있지만 스케줄러에서 제공하는 대부분의 기능은 스코프를 통해 사용할 수 있다.
runTest
runTest는 코루틴을 사용하는 코드를 테스트할 때 사용되며 테스트 블록 내에서 suspend 함수를 호출할 수 있다. runBlocking과 유사하게 동작하지만 몇 가지 차이점이 있다.
- 태스크의 실행 순서는 보존하면서 delay 호출 시 발생하는 지연을 자체적으로 생략한다. 테스트 블록 내에서
delay(3000)
처럼 호출하더라도 테스트 함수가 3초간 지연되지 않고 즉시 실행되며, virtual time만 3000만큼 증가한다. - virtual time 제어: 단순히 delay의 지연을 생략하는 것 뿐만 아니라 virtual time에 대한 세부적인 제어가 가능하다. 예를 들어 virtual time을 특정 시간 만큼 증가시키거나(
advanceTimeBy
), 큐에 들어있는 모든 태스크가 완료될 때 까지 진행하거나(advanceUntilIdle
), 현재 시점에서 호출할 수 있는 태스크를 바로 실행할 수도 있다(runCurrent
). - 자식 코루틴에서 발생한 uncaught exception을 테스트 함수가 종료될 때 던진다.
- 비동기 콜백 대기
launch
and async
테스트에서 사용되는 디스패처는 single-thread 방식으로 동작한다. 따라서 launch, async 등으로 여러 개의 코루틴을 시작하더라도 어떤 코루틴도 다른 코루틴과 함께 병렬적으로 수행되지 않는다. (외부에 다른 스코프를 만들고 코루틴을 생성하면 테스트에서도 병렬적으로 수행할 수 있다)
delay등으로 suspend 되며 실행 순서가 변경되는 코루틴은 virtual time에 의해 스케줄링 된다.
Virtual time 제어
runTest의 테스트 블록 내에서는 테스트의 virtual time을 제어할 수 있다.
currentTime
: 현재 시점의 virtual time 반환.runCurrent()
: 현재 시점에서 스케줄링 된 태스크를 실행.advanceUntilIdle()
: 큐에 남아 있는 모든 태스크를 실행하며 모든 태스크가 종료될 때 까지 virtual time 증가.advanceTimeBy(timeDelta)
: virtual time이 timeDelta 만큼 증가하는 동안 실행할 수 있는 태스크를 실행.
Eagerly entering launch
and async
blocks
테스트가 함수의 동작만 테스트하며 코루틴의 정확한 순서까지는 신경쓸 필요가 없다면, 코루틴을 생성하고 runCurrent
또는 yield
를 호출하는 과정이 번거롭게 여겨질 수 있다.
이 때, UnconfinedTestDispatcher와 함께 runTest를 실행하면 runTest의 top-level에서 생성된 코루틴들이 디스패치될 때 까지 기다릴 필요 없이 즉시 실행할 수 있다.
만약 특정 상황에서 코루틴을 즉시 실행하지 않고 디스패치될 때 까지 대기해야 한다면, 디스패처를 변경하는 것으로 쉽게 달성할 수 있다.
Flow의 테스트
Cold flow
테스트 대상이 일반적인 cold flow라면 쉽게 테스트할 수 있다.
flow에서 생성하는 아이템이 수가 많지 않다면 toList
등으로 생성되는 모든 아이템을 받아 검증할 수 있다.
만약 flow에서 많은 아이템을 생성하고 있다면 flow의 operator를 사용하여 테스트 할 아이템을 제한하는 것이 좋다.
https://developer.android.com/kotlin/flow/test?hl=ko 참고
Hot flow
SharedFlow
와 StateFlow
등 hot flow를 cold flow처럼 테스트하기엔 몇 가지 제약이 있다.
SharedFlow
는 옵저버가 없다면 발행되는 값이 무시되기 때문에 플로우의 옵저버를 먼저 설정하고 동작을 수행해야 한다. 하지만 SharedFlow
는 기본적으로 항상 활성 상태이며 complete 되지 않는다. 따라서 SharedFlow
의 collect 호출 이후의 코드는 무시된다.
값을 생성하고 옵저버를 설정하면 이전의 값을 얻을 수 없고(replay를 설정할 수도 있지만 이렇게 되면 테스트를 위해 프로덕션 코드의 동작을 변경해야 한다), 옵저버를 먼저 설정하면 값을 변경하는 코드에 도달하지 못한다. 결국 테스트를 위해선 SharedFlow
를 다른 코루틴에서 관찰해야 한다.
테스트 예시는 다음과 같다.
기본적으로 TestScope
에서 StandardTestDispatcher
를 사용하기 때문에 completeEvent
를 수집하는 코루틴과 뷰모델에서 값을 발행하는 코루틴은 테스트 블록이 suspend 될 때 까지 실행되지 않는다. assertion 전 코루틴을 실행하기 위해 viewModel.complete()
호출 이후 runCurrent
를 호출하여 스케줄링된 코루틴을 실행시키고 있다.
runCurrent
로 코루틴의 순서를 관리하는것이 번거롭다면 UnconfinedTestDispatcher
를 사용할 수 있다.
테스트 블록에서 사용하는 디스패처와 뷰모델 스코프에서 사용하는 메인 디스패처를 모두 UnconfinedTestDispatcher
로 변경한다. TestScope
를 사용하거나 runTest
에 디스패처를 지정하는 것 중 기호에 따라 선택하면 된다.
StateFlow
의 경우에는 마지막으로 발행한 값을 value
프로퍼티에 저장하고 있기 때문에 SharedFlow
보다 쉽게 테스트할 수 있다.
메인 디스패처를 StandardTestDispatcher
로 지정한 경우엔 runCurrent
등으로 코루틴 실행 순서를 관리할 필요가 있다.
일반적인 테스트의 경우 UnconfinedTestDispatcher
를 사용해도 무방한 듯 하다. 코루틴 실행 순서 문제로 테스트가 실패하는 테스트만 StandardTestDispatcher
를 사용하여 순서를 명시해도 되지 않을까?