
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는 테스트 목적으로 CorutoinDisaptcher 구현입니다.
코루틴의 실행을 예측할 수 있게 도와줍니다. 새로 코루틴을 만들경우 TestDispatchers 를 사용해야 합니다.
핵심 사항: runTest는 TestScope에서 테스트 코루틴을 실행합니다. TestDispatchers는 TestCoroutineScheduler를 사용하여 가상 시간을 제어하고 테스트에서 새 코루틴을 예약합니다. 테스트의 모든 TestDispatchers는 동일한 스케줄러 인스턴스를 사용해야 합니다.
@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())
}

새 코루틴이 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
}
테스트 중인 코드는 디스패처를 사용하여 스레드를 전환하거나 새 코루틴을 시작할 수 있습니다.
다음 예시에서는 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에서 시작된 새 코루틴이 완료되도록 합니다.
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 {
// ...
}
}