코딩

[코루틴 (9)] 코루틴 테스트 전략 & 디버깅

Eastpark 2025. 1. 22. 19:30
728x90
반응형

지난 8편에서는 코루틴 환경에서 발생할 수 있는 동시성 문제와 스레드 안전성을 다루며, Mutex, Semaphore 등을 활용하는 방법을 살펴보았습니다. 이번 글에서는 코루틴 테스트를 효과적으로 진행하기 위한 전략과, 디버깅을 용이하게 만들어주는 기법들을 소개하려고 합니다.

 

핵심 키워드

코루틴 테스트(Unit Test, Instrumented Test)

runTest, Dispatchers.Unconfined

Turbine (Flow Test)

디버깅(Debugging)

DebugProbes, IDE 디버거


1. 왜 코루틴 테스트가 어려울까?

 

코루틴은 비동기적으로 동작하기 때문에, 테스트 시점을 어떻게 맞출지 고민이 필요합니다. 예를 들어,

실행이 끝나기 전에 테스트가 종료되거나,

예외/취소 여부를 제대로 감지하지 못하거나,

Flow를 collect하는 도중 테스트가 끝나버리는

 

등의 문제가 발생할 수 있습니다. 또한, 스레드 전환(Dispatcher) 때문에 결과가 일정하지 않을 수도 있습니다.

이를 방지하기 위해서는 테스트용 DispatcherrunTest 같은 API를 제대로 활용해야 합니다.


2. runTest와 테스트 디스패처

 

2.1 runTest (kotlinx-coroutines-test)

코루틴 테스트 라이브러리인 kotlinx-coroutines-test는 코루틴을 테스트하기 위한 다양한 기능을 제공합니다.

@OptIn(ExperimentalCoroutinesApi::class)
class MyCoroutineTest {

    @Test
    fun testSomething() = runTest {
        val result = doSuspendWork()
        assertEquals("Expected", result)
    }
}

runTest는 테스트 블록에서 코루틴을 실행하고, 내부적으로 시간을 가상으로 조작하거나(Delay Controller), 스케줄링을 제어할 수 있습니다.

테스트가 끝날 때까지 코루틴이 안전하게 동작하고, 결과를 수집할 수 있도록 보장합니다.

 

2.2 Test Dispatcher & Delay Control

StandardTestDispatcher, UnconfinedTestDispatcher 등을 이용해 지연(Delay)을 빠르게 제어하거나, 메인 스레드를 사용하지 않고도 테스트가 가능합니다.

@OptIn(ExperimentalCoroutinesApi::class)
class MyCoroutineTest {

    private val testDispatcher = StandardTestDispatcher()

    @Before
    fun setup() {
        // 메인 디스패처를 테스트 디스패처로 교체
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun testWithDispatcher() = runTest(testDispatcher) {
        val deferred = async {
            // some work
        }
        advanceTimeBy(1000L) // 가상 시간 이동
        // assert ...
    }
}

advanceTimeBy(1000L)로 코루틴 지연을 한 번에 스킵하여, 실제 시간 소모 없이 테스트할 수 있습니다.

728x90

3. Flow 테스트 기법

 

3.1 Turbine 라이브러리

TurbineFlow를 단계별로 검사하기 쉽게 만들어주는 테스트 라이브러리입니다. app.cash.turbine 패키지로 제공되며, Jetbrains Compose 팀에서도 사용 사례가 있습니다.

@Test
fun testFlowWithTurbine() = runTest {
    val testFlow = flow {
        emit(1)
        emit(2)
        delay(100)
        emit(3)
    }
    testFlow.test {
        assertEquals(1, awaitItem())
        assertEquals(2, awaitItem())
        advanceTimeBy(100)
        assertEquals(3, awaitItem())
        awaitComplete()
    }
}

testFlow.test { ... } 블록 안에서 awaitItem(), awaitComplete() 등을 호출해, Flow가 방출하는 이벤트를 순차적으로 검증할 수 있습니다.

advanceTimeBy()를 통해 코루틴의 지연(Delay)도 가상 시간으로 컨트롤 가능합니다.

 

3.2 collect와 Assertions

Flow를 단순히 collect하여 List에 담고, 이후 List를 검사하는 방식도 가능합니다.

다만 Turbine 같은 도구를 쓰면 Flow 이벤트타이밍별로 세밀하게 테스트할 수 있어 편리합니다.


4. Instrumented Test와 Mock

 

4.1 안드로이드 환경에서의 코루틴 테스트

Instrumented Test(에뮬레이터나 실제 기기에서 실행)에서도 코루틴 로직을 테스트할 수 있습니다. 예를 들어, Room + 코루틴 + ViewModel 조합이 정상 동작하는지 통합적으로 검사할 때 사용합니다.

1. Mock or Fake: 네트워크나 DB 부분을 모의 구현으로 대체하거나, 실제 Room을 In-Memory DB로 설정.

2. lifecycleScope, viewModelScope: 실 환경과 동일하게 동작. Instrumented Test에서 ActivityScenario를 사용해 Activity/Fragment를 구동하고, 실 UI 로직을 검증.

 

4.2 Espresso, Compose UI Test

UI 테스트 프레임워크(Espresso, Compose UI Test)와 코루틴을 결합해, UI 상태가 Flow로 변경되는 과정을 검증할 수도 있습니다.

예: onView(withId(R.id.textView)).check(matches(withText("Expected"))) 식으로, Flow의 최종 결과가 UI에 반영되었는지 확인.


5. 디버깅(Debugging) 기법

 

5.1 DebugProbes와 IDE

DebugProbes는 코루틴 디버깅을 위한 API로, 어떤 코루틴이 어떤 상태로 일시 중단됐는지 확인할 수 있게 해줍니다.

DebugProbes.install()

// 실행 중간에...
DebugProbes.dumpCoroutines()

dumpCoroutines()는 현재 코루틴들의 상태를 콘솔에 출력합니다.

Android Studio나 IntelliJ IDE에서는 코루틴 디버거가 점차 개선되고 있어, Coroutine Tab에서 코루틴들의 스택 상태를 시각적으로 파악할 수 있습니다.

 

5.2 Logging과 Thread name

코루틴 실행 시 CoroutineName("MyTask")을 할당하면 로깅에서 구분하기가 수월합니다.

로깅 라이브러리를 적극 활용해, “어떤 코루틴에서” “어떤 스레드”에서 작업이 이뤄졌는지 쉽게 추적 가능하도록 설계합니다.

launch(CoroutineName("DataLoader") + Dispatchers.IO) {
    // ...
}
반응형

6. Test & Debugging Checklists

 

1. 테스트 디스패처 활용

runTest, StandardTestDispatcher, advanceTimeBy 등을 적극 사용해 비동기 코드를 빠르고 안정적으로 검증.

 

2. Flow 테스트

터빈(Turbine) 라이브러리나 collect 후 결과 리스트 비교.

예외/완료 시점, 타이밍별 이벤트 순서까지 꼼꼼히 확인.

 

3. Instrumented Test

실제 환경(에뮬레이터/기기)에서 Room, 네트워크, ViewModelScope 등 통합 검증.

 

4. 디버깅

코루틴 디버거, DebugProbes를 통해 코루틴 상태 추적.

로깅 시 CoroutineName과 스레드 명시로 가독성을 높임.


마무리 및 다음 예고

 

이번 [9편]에서는 코루틴 테스트와 디버깅에 관한 주요 기법을 살펴보았습니다.

테스트: runTest, TestDispatchers, Turbine 등을 이용하면 비동기 흐름을 제어하고 검증하기 용이해진다.

디버깅: DebugProbes나 IDE의 Coroutine Tab, 로깅을 통해 코루틴 상태와 스택을 추적할 수 있다.

 

다음 10편에서는 고급 주제 & 퍼포먼스 최적화를 다룰 예정입니다. “코루틴 퍼포먼스 모니터링”, “DebugProbes 심화”, “CoroutineContext 커스터마이징” 등 보다 깊이 있는 내용을 함께 살펴보겠습니다.


시리즈 전체 목차

0. 코루틴 탄생 배경과 기본 개념

1. 코루틴 기초(suspend, CoroutineScope, Dispatcher)

2. Structured Concurrency와 코루틴 스코프 관리

3. 예외 처리와 취소(Cancellation) 기법

4. 코루틴 채널(Channel)과 select

5. Flow 심화(Flow, StateFlow, SharedFlow)

6. 안드로이드 실무 적용(Retrofit, Room, ViewModel)

7. 동시성 문제 해결(Mutex, Semaphore)

8. 코루틴 테스트 & 디버깅 전략

9. 고급 퍼포먼스 최적화

10. WorkManager + 코루틴 연동

11. LifecycleScope & 안드로이드 생명주기 대응

12. Compose + 코루틴 + Flow 시너지

13. Compose Navigation + 코루틴 취소/재시작 사례

728x90
반응형