Coroutine 1.6 Test API 사용하기

hongbeom
hongbeomi dev
Published in
8 min readJul 10, 2022

--

kotlinx.coroutines 1.6에서 새로운 테스트 API가 도입되었으며, 이전에 쓰이던 API는 이제 더 이상 사용되지 않습니다. 이전 API를 사용하면 곧 deprecated 에러가 발생하며, 2022년 말에 완전히 제거될 예정입니다.

Photo by Maksim Shutov on Unsplash

지난 21년 12월 말, Jetbrains에서 kotlinx.coroutines의 1.6 버전이 공개되었습니다. 멀티플랫폼을 지원하는 새로운 kotlinx-coroutines-test API이 등장했는데, 어떤 API 들이 추가되었는지, 이전 버전의 API을 쓰던 프로젝트에서는 어떻게 migration 하여 사용해야 하는지 살펴보겠습니다.

🏃🏻 runTest

새로운 API의 진입점인 runTest 코루틴 빌더를 먼저 살펴보겠습니다. 이 빌더는 이전의 runBlockingTest를 대체하며, 모든 플랫폼에서 코루틴 코드를 테스트하는데 사용할 수 있습니다.

runTest 함수는 TestResult를 리턴하는 함수인데, 이 값은 JVM 및 네이티브에서는 Unit으로 나타나지만, JS에서는 Promiss가 되며 테스트 실행 함수가 테스트가 끝날 때까지 기다리지 않는다는 사실을 반영합니다.

🕰 TestCoroutineScheduler

다음은 TestCoroutineScheduler입니다. 이 클래스는 딜레이를 건너뛰는 동작을 제공하는 테스트에 유용하게 사용할 수 있습니다. 우리가 사용하는 TestDispatcher는 스케줄러로 파라미터화 되는데, 여러 개의 디스패처가 동일한 스케줄러를 공유할 수 있어서 테스트 중 가상의 시간에 대한 정보가 동기화됩니다.

advanceTimeBy 함수를 통해 코루틴이 실행되는 시간을 앞당길 수도 있고, advanceUntilIdle 함수를 통해 모든 예약되어 있는 작업을 실행하거나, 가능한 빨리 실행되도록 예약되었지만 아직 디스패치 되지 않은 작업을 runCurrent 함수를 통해 실행할 수도 있습니다.

👀 TestScope

테스트 코루틴을 시작하기 위한 CoroutineScope입니다. runTest에서 이 인터페이스의 구체를 생성하여 테스트 스코프가 만들어지며, 이 스코프는 다음 기능을 제공합니다.

  • 해당 스코프의 coroutineContext에는 가상 시간을 조정하기 위해서 TestCoroutineScheduler를 사용하여 딜레이 건너뛰기를 지원하는 코루틴 디스패처가 포함되어 있습니다. 이 스케줄러는 runTest 블록 안에서 testScheduler 프로퍼티로 접근할 수 있으며, 더 편리하게 사용할 수 있도록 익스텐션 메서드가 정의되어 있습니다. (TestScope.currentTime, TestScope.runCurrent 등등..)
  • runTest 내부에서 이 스코프의 자식 코루틴에서 발생한 포착되지 않은 예외는 테스트가 끝날 때 보고됩니다. runTest 외부에서 자식 코루틴이 포착하지 못한 예외를 throw하는 것은 유효하지 않습니다.

vs TestCoroutineScope

  • TestScopecleanupTestCoroutines와 동일한 기능을 따로 제공하지 않으므로 runTest가 필수적으로 호출되어야 사용할 수 있습니다.
  • TestCoroutineScope.advanceTimeBy는 가상 시간을 앞당긴 후 TestCoroutineScheduler.runCurrent도 호출합니다.
  • TestCoroutineDispatcher에서 지원했던 디스패처 일시 중지를 지원하지 않습니다. 디스패처를 일시 중지하는 대신, withContext를 사용하여 StandardTestDispatcher(뒤에 나옵니다.)처럼 기본적으로 일시 중지되는 디스패처를 실행할 수 있습니다.
  • 처리되지 않은 예외에 접근할 수 없습니다.

🤝 TestDispatcher

TestDispatcherTestCoroutineScheduler에 의해 딜레이가 제어되는 CoroutineDispatcher입니다. 2가지로 나뉩니다.

  • StandardTestDispatcher : 특별한 동작이 없는 단순한 디스패처입니다. 이 디스패처는 자체적으로 작업을 실행하지 않고 항상 스케줄러에게 작업을 전달합니다. 실제로 이는 launch 또는 async 블록이 즉시 시작되지 않음을 의미하며 runTest 내부에서 TestScope 혹은 스케줄러가 제공하는 함수를 통해 시작되도록 제어할 수 있습니다.
  • UnconfinedTestDispatcher : Dispatcher.Unconfined 처럼 작동하는 디스패처입니다. 코루틴이 시작되는 순서를 보장하지 않지만, 코루틴이 즉각적으로 시작되기 때문에 테스트 코드에서 runCurrentadvanceUntilIdle 같은 함수를 수동으로 호출할 필요가 없습니다.

StandardTestDispatcher vs UnconfinedTestDispatcher

Standard: 실행 순서에 대한 완전한 제어 가능, 코루틴이 자동으로 실행되지 않음

Unconfined : 실행 순서에 대한 완전한 제어 불가능, 코루틴이 자동으로 실행됨

  • 코루틴의 실행 순서를 테스트해야하며, 실행되는 코루틴과 시기의 세밀한 제어가 필요하다면 StandardTestDispatcher
  • 그렇지 않고 단순하고 간결한 테스트라면 UnconfinedTestDispatcher

🎲 MainDispatcherRule

기존에 우리는 Unit 테스트에서 viewModelScope에서 사용되는 Main 디스패처인 Android UI 스레드를 사용하지 못하므로 Main 디스패처를 TestCoroutineDispatcher로 바꿔치기 하는 Rule을 작성하여 사용했을 것입니다.

새로운 API를 사용할 때는 아래 코드처럼 변경해주면 기존의 API를 대체할 수 있으며 테스트 클래스에서 기존 처럼 사용할 수 있습니다.

😪 Lazy Start

일부 테스트에서는 Main 디스패처 코루틴에 대한 게으른 스케줄링이 필요할 수 있습니다. 이전 API에서 우리는 이런 테스트에 대해 pauseDispatcher/resumeDispatcher를 사용하여 새로운 코루틴이 너무 일찍 실행되는 것을 방지하곤 했습니다.

새로운 API로 동일한 테스트를 수행하려면, Main 디스패처를 StandardTestDispatcher로 설정해야하므로 MainDispatcherRule에서 사용하는 것과는 다른 TestDispatcher를 지정해주어야 합니다.

따라서 우리는 runTest 블록으로 테스트 코드 본문을 감싸고, 테스트가 시작되기 전에 우리가 MainDispatcherRule에 지정했던 UnconfinedTestDispatcherStandardTestDispatcherMain 디스패처를 교체해줌으로써 이전 API와 동일한 동작으로 테스트를 실행할 수 있습니다.

이 방법은 우리가 게으르게 시작되길 원하는 테스트 코드에만Dispatchers.setMain함수를 통해 디스패처를 변경해줄 수 있습니다.

🧹 cleanup 코드 제거

우리가 작성한 테스트 중에는 테스트 내부의 코루틴이 완료되기를 명시적으로 기다리는 코드가 존재할 수 있습니다. (ex: 비동기적으로 진행되는 작업을 명시적으로 취소해주어야 하는 경우)

이전 API에서는 coroutineRule.testDispatcher.advanceUntilIdle() 함수를 마지막 줄에 추가하여 이 코루틴들의 취소 작업을 대기하도록 했지만, runTest를 사용한다면 해당 코드를 작성할 필요가 없습니다. runTest는 테스트 코루틴의 자식 코루틴 및 TestDispatcher에서 실행되는 모든 코루틴의 작업을 자동으로 대기합니다. 즉, 코루틴이 완료되기를 기다리는 모든 cleanup 관련 코드를 제거할 수 있는 것입니다.

참고

--

--