시배's Android

Kotlin Coroutines Deep Dive | 15장. 코틀린 코루틴 테스트하기 본문

Book/Kotlin Coroutines Deep Dive

Kotlin Coroutines Deep Dive | 15장. 코틀린 코루틴 테스트하기

si8ae 2024. 2. 12. 16:08

TestCoroutineScheduler와 StandardTestDispatcher

  • TestCoroutineScheduler는 delay를 가상 시간 동안 실행하여 실제 시간이 흘러간 상황과 동일하게 작동하기 때문에 정해진 시간만큼 기다리지 않도록 변경할 수 있습니다.
  • 코루틴에서 TestCoroutineScheduler를 사용하려면, 이를 지원하는 디스패처를 사용해야 합니다.
  • 일반적으로 StandardTestDispatcher를 사용합니다.
  • 테스트 디스패처로 시작된 코루틴은 가상 시간만큼 진행되기 전까지 실행되지 않습니다.
  • StandardTestDispatcher는 TestCoroutineScheduler를 만들기 때문에 명시적으로 만들지 않아도 됩니다.
  • StandardTestDispatcher는 직접 시간을 흐르게 하지 않는다는 사실을 명시해야 합니다.
  • 시간을 흐르게 하지 않으면 코루틴이 다시 재개되지 않습니다.
  • 시간을 흐르게 하는 또 다른 방법은 advancedTimeBy에 일정 밀리초를 인자로 넣어 주는 것입니다.
  • 2밀리초와 정확히 일치하는 시간에 예정된 연산을 재개하려면 runCurrent함수를 추가로 호출하면 됩니다.

runTest

  • TestScope에서 코루틴을 시작하고 즉시 유휴 상태가 될 때까지 시간을 흐르게 합니다.
  • runTest 함수는 다른 함수처럼 스코프를 만들며, 자식 코루틴이 끝날 때까지 기다립니다.
  • 절대 끝나지 않는 프로세스를 시작한다면 테스트 또한 종료되지 않습니다.
  • backgroundScope는 테스트가 기다릴 필요 없는 모든 프로세스를 시작할 떄 사용합니다.

취소와 컨텍스트 전달 테스트하기

  • 특정 함수가 구조화된 동시성을 지키고 있는지 테스트하려면,
  • 중단 함수로부터 컨텍스트를 받안 뒤, 컨텍스트가 기대한 값을 가지고 있는지와 잡이 적절한 상태인지 확인하는 것이 가장 쉬운 방법입니다.
@Test
fun `should support cancellation`() = runTest {
	var job : Job? = null 
    val parentJob = launch {
    	listOf("A").mapAsync {
        	job = currentCoroutineContext().job
            delay(Long.MAX_VALUE)
        }
    }
    delay(1000)
    parentJob.cancel()
    assertEquals(true, job?.isCancelled)
}

UnconfinedTestDispatcher

  • StandardTestDispatcher는 스케줄러를 사용하기 전까지 어떤 연산도 수행하지 않는다는 것이 가장 큰 차이점입니다.
  • UnConfinedTestDispatcher는 코루틴을 시작했을 때 첫 번째 지연이 일어나지 전까지 모든 연산을 즉시 수행합니다.
fun main() {
	CoroutineScope(StandardTestDispatcher()).launch {
    	print("A")
        delay(1)
        print("B")
    }
}

디스패처를 바꾸는 함수 테스트하기

@Test
fun `should change dispatcher`() = runBlocking {
	// given
    val csvReader = mockk<CsvReader>()
    val startThreadName = "MyName"
    var usedThreadName : String? = null
    every {
    	casvReader.readCsvBlocking(
        	aFileName,
        	GameState::class.java
        )
    } coAnswers {
    	usedThreadName = Thread.currentThread().name
        aGameState
    }
    val saveReader = SaveReader(csvReader)
    
    // when
    withContext(newSingleThreadContext(startThreadName)){
    	saveReader.readSave(aFileName)
    }
    
    // then
    assertNotNull(usedThreadName)
    val expectedPrefix = "DefaultDispatcher-worker-"
    assert(usedThreadName!!.startsWith(expectedPrefix))