Android coroutine Test

day_0893·2023년 9월 17일

Android Coroutine

목록 보기
4/4
post-thumbnail

안드로이드 디벨로퍼 URL

1.dependencies 추가
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")

테스트에서 정지 함수 호출

suspend 함수를 호출을 위해 runTest를 사용할 수 있다.

suspend fun fetchData(): String {
    delay(1000L)
    return "Hello world"
}

@Test
fun dataShouldBeHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
}
  • 최상위 테스트 코루틴 외 새로운 코루틴을 만들 때 적절한 TestDispatcher을 선택하여 새 코루틴이 예약되는 방식을 제어해야 합니다.

  • 코루틴 실행을 다른 디스패처리 이동하면 runTest는 일반적으로 작동되지만 여러 스레드 실행의 예측가능성이 떨어집니다. -> TestDispatcher을 사용해야합니다.

TestDispatcher

TestDispatcher는 테스트 목적으로 CorutoinDisaptcher 구현입니다.
코루틴의 실행을 예측할 수 있게 도와줍니다. 새로 코루틴을 만들경우 TestDispatchers 를 사용해야 합니다.

핵심 사항: runTest는 TestScope에서 테스트 코루틴을 실행합니다. TestDispatchers는 TestCoroutineScheduler를 사용하여 가상 시간을 제어하고 테스트에서 새 코루틴을 예약합니다. 테스트의 모든 TestDispatchers는 동일한 스케줄러 인스턴스를 사용해야 합니다.

StandardTestDispatcher

    @Test
    fun standardTest()= runTest{
        val userRepo = UserRepository()
        launch { userRepo.register("Alice") }
        launch { userRepo.register("Bob") }

         assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) //error

launch{}는 비동기 실행기기 때문에
advanceUntilIdle()를 사용하여 비동기 상태를 기다린 후 실행한다.

  @Test
    fun standardTest()= runTest{
        val userRepo = UserRepository()
        launch { userRepo.register("Alice") }
        launch { userRepo.register("Bob") }

//         assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) //error

        advanceUntilIdle() // Yields to perform the registrations
        assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers())
    }

  • 참고: launch 호출에서 반환된 Job 인스턴스를 join하여 어설션 실행 전에 새 코루틴이 완료되도록 할 수도 있습니다.

UnconfinedTestDispatcher

새 코루틴이 UnconfinedTestDispatcher에서 실행되면서 빠르게 시작됩니다. 코루틴 빌더가 반환될 때 까지 기다리지 않고 즉시 실행됩니다.

핵심 사항: UnconfinedTestDispatcher는 새 코루틴을 빠르게 실행하며 코루틴을 사용한 간단한 테스트에 적합합니다.

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

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

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

UnconfinedTestDispatcher는 새 코루틴을 빠르게 시작하지만 그렇다고 해서 완료될 때까지 빠르게 실행하는 것은 아닙니다. 새 코루틴이 정지되면 다른 코루틴이 실행을 다시 시작합니다.

예를 들어 이 테스트 내에서 실행된 새 코루틴은 Alice를 등록하지만 delay가 호출되면 정지됩니다. 이를 통해 최상위 코루틴이 어설션을 계속 진행할 수 있고 테스트는 Bob이 아직 등록되지 않았으므로 실패합니다.

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

    launch {
        userRepo.register("Alice")
        delay(10L)
        userRepo.register("Bob")
    }

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

테스트 디스패처 삽입

테스트 중인 코드는 디스패처를 사용하여 스레드를 전환하거나 새 코루틴을 시작할 수 있습니다.

  • 핵심 사항: 테스트에서 실제 디스패처를 TestDispatchers 인스턴스로 바꿔 모든 코드가 단일 테스트 스레드에서 실행되도록 합니다.
  • 참고: 테스트에서 Main 디스패처를 교체하는 방법에 관한 자세한 내용은 Main 디스패처 설정 섹션을 참고하세요.
  • 주의: 테스트 내에서 TestDispatchers를 얼마든지 만들어 사용할 수 있지만 모두 동일한 스케줄러를 공유해야 합니다. 여러 개의 스케줄러를 만들지 않도록 주의하세요.

다음 예시에서는 initialize 메서드에서 IO 디스패처를 사용하여 새 코루틴을 만들고 fetchData 메서드에서 호출자를 IO 디스패처로 전환하는 Repository 클래스를 확인할 수 있습니다.

// Example class demonstrating dispatcher use cases
class Repository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
    private val scope = CoroutineScope(ioDispatcher)
    val initialized = AtomicBoolean(false)

    // A function that starts a new coroutine on the IO dispatcher
    fun initialize() {
        scope.launch {
            initialized.set(true)
        }
    }

    // A suspending function that switches to the IO dispatcher
    suspend fun fetchData(): String = withContext(ioDispatcher) {
        require(initialized.get()) { "Repository should be initialized first" }
        delay(500L)
        "Hello world"
    }
}

테스트에서 TestDispatcher 구현을 삽입하여 IO 디스패처를 대체할 수 있습니다.

아래 예시에서는 저장소에 StandardTestDispatcher를 삽입하고 advanceUntilIdle을 사용하여 계속 진행하기 전에 initialize에서 시작된 새 코루틴이 완료되도록 합니다.

  • 주의: 새 코루틴이 완료될 때까지 진행하는 것이 가능한 유일한 이유는 새 코루틴이 TestDispatcher를 사용하기 때문입니다. 이를 통해 위 예시에서 initialize 메서드가 잘 설계된 API가 아님을 알 수 있습니다. 호출자가 기다려야 하는 비동기 작업을 시작하지만 이 작업이 완료되면 호출자에게 알릴 방법이 없습니다.
class RepositoryTest {
    @Test
    fun repoInitWorksAndDataIsHelloWorld() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val repository = Repository(dispatcher)

        repository.initialize()
        advanceUntilIdle() // Runs the new coroutine
        assertEquals(true, repository.initialized.get())

        val data = repository.fetchData() // No thread switch, delay is skipped
        assertEquals("Hello world", data)
    }
}

TestDispatcher에서 시작된 코루틴은 initialize를 사용하여 수동으로 진행할 수 있습니다. 그러나 프로덕션 코드에서는 불가능하거나 바람직하지 않습니다.
대신 이 메서드는 정지 되거나 또는 Deffered 값을 반환하도록 다시 설계해야합니다.

class BetterRepository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
    private val scope = CoroutineScope(ioDispatcher)

    fun initialize() = scope.async {
        // ...
    }
}

0개의 댓글