본 게시글은 Kt.Academy의 Kotlin Coroutines DEEP DIVE의 요약본입니다.
목차
코루틴 테스트
코루틴을 테스트 하는 것은 일반 함수를 테스트 하는 것과 일반적으론 크게 다르지 않습니다.
가령 아래와 같은 예시의 유즈케이스를 테스트 한다고 해보겠습니다.
이 유즈 케이스를 테스트하기 위해선 유즈 케이스가 참조하는 UserDataRepository
의 Fake를 생성합니다. (Mock 사용해도 OK)
그리고 다음과 같이 테스트 코드를 생성 할 수 있습니다.
테스트 방법론은 여러가지고 팀 마다 방법이 다르므로, 이 테스트 방법을 바이블로 할 필요는 없습니다. 이 예제는 어디까지나 코루틴을 테스트하는 방법은 일반 메소드 테스트와 다르지 않다는 것을 보여주기 위함 입니다.
테스트 시간 단축하기
기획 요구 사항에 의해 프로덕션 코드에 delay(TimeMilies)
를 줬다고 가정해보겠습니다.
이건 우리의 프로덕션에서 필요한 부분이지, 테스트에서 까지 delay 시간 만큼의 테스트를 지연 시킬 필요는 없습니다.
Kotlin은 이런 부분에서 테스트 시간을 단축할 수 있는 방법을 제공하고 있습니다.
TestCoroutineScheduler와 StandardTestDispatcher (kotlinx-coroutines-test:1.6.0 이상)
delay()
는 일시정지 되었다가 일정 시간 뒤에 resume됩니다. 우리는 TestCoroutineScheduler
를 통해 테스트 코드 속 가상 시간을 임의로 조절합니다.
TestCoroutineScheduler
를 사용하기 위해 별도의 Dispatcher
에 담아야합니다.
이를 위한 표준적인 Dispatcher
는 StandardTestDispatcher
입니다.
StandardTestDispatcher
를 사용 하면서 명심할 점은, StandardTestDispatcher
는 자동 실행이 되지 않는다는 점입니다.
TestCoroutineScheduler
는 개발자가 지정한 시간 만큼만 코루틴이 실행됩니다. 그렇기 때문에 별도의 시간을 정해주지 않으면 코루틴이 실행되지 않습니다.
코루틴을 실행하기 위한 함수는 다음과 같습니다.
TestCoroutineScheduler.advanceUntilIdle()
: 전체 코루틴 Task를 실행합니다.TestCoroutineScheduler.advanceTimeBy(TimeMilli)
: 해당 시간만큼만 코루틴 Task를 실행합니다.TestCoroutineScheduler.runCurrent()
: 현재 코루틴 Task를 실행합니다.
또한 이 뿐만 아니라, viewModelScope
를 사용하는 코루틴에 대해서 테스트를 해야할 때, JVM에는 메인 스레드라는 개념이 없으므로, Dispatcher
를 변경 시켜줘야 합니다.
이 때, StandardTestDispatcher
로 변경 시킴으로써, delay를 무시하고 유닛 테스트를 할 수 있습니다.
TestScope(kotlinx-coroutines-test:1.6.0 이상)
TestScope
는 TestCoroutineScheduler
+ StandardTestDispatcher
가 적용되어 있는 CoroutineScope
입니다.
앞서 말한 advanceUntilIdle()
, advanceTimeBy()
, runCurrent()
를 모두 사용할 수 있습니다.
runTest(kotlinx-coroutines-test:1.6.0 이상)
runTest()
는 TestScope
+ advanceUntilIdle()
가 합쳐진 runBlocking()
이라고 생각하면 됩니다.
코루틴 유닛 테스트를 할 경우에 가장 일반적으로 사용될 함수로, 다음과 같이 StandardTestDispatcher
, TestScope
등을 정의할 필요 없이 runTest()
만으로 쉽고 간단하게 코루틴 테스트를 할 수 있습니다.
runTest로 코루틴 실행 중에 테스트 하기
runTest()
는 suspend
block이 내부 Task Heap에 다 담긴 다음에, advanceUntilIdle()
가 호출되어 Task에 담긴 코루틴을 실행 하는 구조입니다.
그렇기에 suspend
block 내부 코루틴은 즉시 실행되지 않기 때문에, 시간을 조절해서 테스트를 할 수 있습니다.
아래와 같은 코드를 테스트 한다고 가정하겠습니다.
각 suspend
함수 내부에는 delay(1_000)
으로 구현 했다는 가정 하에, 정확한 시간으로 progressBarVisible
의 value가 잘 변경 되는지를 테스트 하려고 합니다.
그래서 runTest()
내부에서 advanceTimeBy()
, runCurrent()
, advanceUntilIdle()
을 이용하여 다음과 같이 정확한 시간에 progressBarVisible
의 상태 변경을 체크 할 수 있습니다.
자식 코루틴이 아닌 경우에 테스트 하기
runTest()
내부에서 자식 코루틴이 아닌 경우에 대해서 테스트 해야 할 경우가 있습니다.
아주 대표적으로 ViewModel
을 검증할 때 순서가 어긋나는 경우가 발생합니다.
로그 결과로 보았을 때도 순서가 맞지 않게 됩니다.
이럴 경우엔 앞서 설명 했던 advanceUntilIdle()
을 사용하면 순서를 맞출 수 있습니다.
그런데 신기하지 않나요?? MainDispatcherRule
로 적용된 Dispatcher.Main
의 StandardTestDispatcher
의 Scheduler와 runTest()
가 receiver로 전달하는 TestScope
의 Scheduler는 둘 다 TestCoroutineScheduler
지만, 인스턴스가 다를탠데
runTest
의 TestScope.advanceUntilIdle()
로 동기화가 맞춰진다는게 신기합니다.
이게 가능한 이유는 StandardTestDispatcher()
팩토리 함수 내부를 보면 알 수 있습니다.
가장 핵심 역할을 하는 TestCoroutineScheduler
가 싱글톤으로 관리되고 있기 때문입니다.
runTest
내부에서도 StandardTestDispatcher
팩토리 함수를 통해 생성하고 있으므로 runTest
와 MainDispatcherRule()
의 Scheduler는 동일합니다.
그렇기에 runTest
의 TestScope.advanceUntilIdle()
로 다 제어가 가능한 것입니다.
당연히 로그 결과를 봐도 동일한 인스턴스임을 확인 할 수 있었습니다.
이는 모두다 기본 값으로 정의 할 때 동기화가 이뤄지므로, 만약 별도의 Dispatcher
를 지정한다면 결과는 달라지므로 유의할 필요가 있습니다.