Coroutine/Flow Test

seong-hwan Kim
shDev
Published in
9 min readDec 24, 2021

코루틴 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에 의해 딜레이가 제어되는 CoroutineDispatcher
  • Dispatchers.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

SharedFlowStateFlow 등 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 를 사용하여 순서를 명시해도 되지 않을까?

--

--