Kotlin Coroutine 테스트 코드 작성 예제

Picbel·2022년 8월 28일
3

Concurrency Programming

목록 보기
3/4
post-thumbnail
post-custom-banner

이번 포스팅은 코틀린 코루틴을 사용한 코드를 테스트 하는 방법입니다.
사실 아주 크게 특별할 껀 없습니다.

dependencies {
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}

먼저 코루틴 테스트 모듈을 설치하여 줍니다.
이제 테스트시 이용하는 코루틴 스코프는 runTest를 이용하여 테스트할 예정입니다.
테스트시 유의해야할 가장 큰점은 다음과 같습니다.

StandardTestDispatcher는 코루틴을 가장 마지막에 실행한다.

StandardTestDispatcher는 runTest에서 default 디스페쳐 입니다.
runTest에서 테스트코드를 래핑하면 기본 정지함수를 테스트할 수 있고 코루틴의 지연을 자동으로 건너뜁니다.
즉 delay함수나 기타 지연이 생기지 않습니다.

코드로 확인하여 보겠습니다.

private val oneDay = 86400000L

@ExperimentalCoroutinesApi
@Test
fun `기본 테스트 디스패처는 코루틴 스코프를 가장 마지막에 실행한다`() = runTest {
	println("test start")
	launch {
    	delay(oneDay)
        println("launch 1")
    }

	async {
    	println("launch 2")
    }

	println("test end")
}

lanuch에서 delay를 하루를 걸었지만 실제로 실행하여보면 테스트는 바로 종료됩니다.
print도 보면 test end가 launch같은 코루틴보다 먼저 작성되는 모습을 확인하실수 있습니다.

StandardTestDispatcher에서 새 코루틴을 시작하면 코루틴이 기본 스케줄러의 대기열에 추가되어 테스트 스레드를 사용할 수 있을 때마다 실행됩니다.

따라서 다음과 같은 코드는 실행시 문제가 됩니다.

@Test
fun standardTest() = runTest {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers())  // ❌ Fails
}

검증부분이 먼저 실행되기 때문에 검증전 코루틴 스코프를 실행시켜야합니다.
여러가지 방법이 있지만 먼저 StandardTestDispatcher에선 다음과 같은 함수를 사용하여 테스트내 코루틴을 실행합니다.

  • advanceUntilIdle : 테스트내 모든 코루틴 스코프를 전부 실행합니다.
  • advanceTimeBy: 주어진 양만큼 가상 시간을 진행하고 가상 시간의 해당 지점 전에 실행되도록 예약된 코루틴을 실행합니다.
  • runCurrent: 현재 가상 시간에 예약된 코루틴을 실행합니다.

이러한 특성을 이용하여 코루틴의 실행시점을 조작할수 있습니다.

 @ExperimentalCoroutinesApi
 @Test
 fun `테스트의 모든 코루틴을 실행합니다`() = runTest {

	launch {
    	println("launch 1")
    }

	launch {
    	println("launch 2")
    }

	advanceUntilIdle()

	println("test end")
}

advanceTimeBy을 이용하면 mock등으로 감싼 코루틴에 delay를 걸어 조작도 가능합니다.

@ExperimentalCoroutinesApi
@Test
fun `테스트를 가상시간 만큼 진행시킵니다`() = runTest {

	launch {
    	delay(oneDay*2)
        println("launch 1")
    }

	launch {
    	delay(halfDay)
        println("launch 2")
    }

	advanceTimeBy(oneDay)

	println("test end")
}


코드를 보시면 하루만큼 2일간 딜레이를 건 코루틴과 반나절동안 딜레이를 건 코루틴을 생성하고 advanceTimeBy을 이용하여 가상시간 하루만큼 시간을 당겻습니다.
로그의 print를 보면 반나절인 launch 2가 찍히고 테스트가 종료된후에 스케줄러가 코루틴을 실행시킵니다.

lanch의 반환인 job이나 async의 반환인 Deferred에서 각각 join, await를 이용하는 방법도 있긴 합니다만 해당 방법은 테스트 객체에서 코루틴스코프에 접근이 가능해야합니다.
만약 코루틴이 함수 안쪽에 숨어있다면 위의 메서드를 이용하여 코루틴을 실행시켜야합니다.


UnconfinedTestDispatcher

해당 테스트 코루틴 디스페처는 코루틴을 생성하는 즉시 바로 실행합니다.
즉 코루틴 빌더가 반환될 때 까지 기다리지 않고 즉시 실행됩니다.
따라서 StandardTestDispatcher와 달리 코루틴의 실행주기를 조작할순 없지만 코드가 굉장히 간결해집니다.

@Test
fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
}

위의 예제코드를 하나를 UnconfinedTestDispatcher로 변환하여 실행해보겠습니다.

 @ExperimentalCoroutinesApi
 @Test
 fun `Unconfined은 모든 코루틴을 생성 즉시 실행합니다`() = runTest(UnconfinedTestDispatcher()) {
		println("test start")
        
        launch {
        	delay(oneDay)
            println("launch 1")
        }

        launch {
            println("launch 2")
        }

        println("test end")
    }


위의 예제 기본 테스트 디스패처는 코루틴 스코프를 가장 마지막에 실행한다의 코드에서 디스페쳐만 UnconfinedTestDispatcher으로 변경하고 실행한 결과입니다.
테스트내에있는 코루틴을 실행하고 종료전 시간을 무시하고 실행하는것을 확인 할 수 있습니다.


분량 조절 실패로 인한 추후 테스트 디스패처 관한 상세한 내용은 이후에 다루도록하겠습니다.

서버 단에서 코루틴을 사용 시 ui 스레드라는 개념이 없기 때문에 테스트 디스패처를 직접 구현해야 할 일은 없었습니다.(현재까지는)
요청받은 스레드에서 비즈니스 로직 등 로직을 관리하기 때문이죠.
일반적인 mvc 모델이 아닌 webflux같은 경우라면 달라질 수도 있을 것 같습니다만...webflux에서 굳이 코루틴을 사용해야 하는지...?

출처 Android에서 Kotlin 코루틴 테스트

profile
Software Developer
post-custom-banner

0개의 댓글