[Android] ์ฝ”๋ฃจํ‹ด ํ…Œ์ŠคํŠธ

๐Ÿ’ก์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค ํ™œ๋™ ์ค‘์— ๊ฒฝํ—˜ํ•œ ์ฝ”๋ฃจํ‹ด ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•˜์—ฌ ๊ธฐ๋กํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

RunTest?

  • runTest๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋‹จ์ผ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  • Kotlin Coroutines Test ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋ฉฐ, ๋น„๋™๊ธฐ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์‹œ๊ฐ„์„ ์ •ํ™•ํ•˜๊ฒŒ ์ œ์–ดํ•˜๊ณ  ์˜ˆ์™ธ๋‚˜ ์ง€์—ฐ ์ž‘์—…์„ ํšจ๊ณผ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

runBlockingTest

  • Coroutine 1.6 ์ด์ „ ๋ฒ„์ „์—์„œ ์ฝ”๋ฃจํ‹ด์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜์—ˆ์ง€๋งŒ, 1.6 ์ดํ›„ ๋ถ€ํ„ฐ๋Š” runBlockingTest๊ฐ€ Deprecated ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— runTest๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • runTest๋ฅผ ํ™œ์šฉํ•  ๊ฒฝ์šฐ ๋” ๋‚˜์€ ์‹œ๊ฐ„ ์ œ์–ด ๋ฐ ์•ˆ์ „์„ฑ์„ ์ œ๊ณตํ•˜๋ฉฐ, ํƒ€์ž„์•„์›ƒ๊ณผ ์‹œ๊ฐ„ ๊ด€๋ จ ์ž‘์—…์˜ ๋™์ž‘์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŠน์ง•

  • runTest๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 60์ดˆ์˜ ํƒ€์ž„์•„์›ƒ์„ ์ ์šฉํ•˜๋ฉฐ, ํ…Œ์ŠคํŠธ๊ฐ€ ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ฑฐ๋‚˜ ๋ฌดํ•œ ๋Œ€๊ธฐ์— ๋น ์งˆ ๊ฒฝ์šฐ ๊ฐ•์ œ๋กœ ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค.
  • delay, withTimeout ๋“ฑ์˜ ์ฝ”๋ฃจํ‹ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์‹œ๊ฐ„ ๊ด€๋ จ ํ•จ์ˆ˜๋“ค์ด ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ๋” ๋น ๋ฅด๊ฒŒ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ ์ค‘ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์žก์•„๋‚ด๊ณ , ์ฝ”๋ฃจํ‹ด์ด ์ž˜๋ชป๋œ ์ƒํƒœ๋กœ ๋๋‚˜์ง€ ์•Š๋„๋ก ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

Gradle

  • runTest๋ฅผ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"

runTest ํ™œ์šฉ

  • runTest ํ•จ์ˆ˜ ์•ˆ์—์„œ ์ฝ”๋ฃจํ‹ด์„ ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋‚ด๋ถ€์˜ delay(1000)์€ ์‹ค์ œ๋กœ 1์ดˆ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ์ฆ‰์‹œ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
  • delay(1000)์„ ํ†ตํ•ด ์ฝ”๋ฃจํ‹ด์ด 1์ดˆ ๋™์•ˆ ์ง€์—ฐ๋˜์ง€๋งŒ, runTest์˜ ํ…Œ์ŠคํŠธ ์‹œ๊ฐ„ ํ๋ฆ„ ์ œ์–ด ๊ธฐ๋Šฅ์œผ๋กœ ์ฆ‰์‹œ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋” ๋น ๋ฅด๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
class CoroutineTest {

    @Test
    fun `test coroutine with runTest`() = runTest {
        val result = fetchData()
        assertEquals("Success", result)
    }

    // ์˜ˆ์‹œ ์ฝ”๋ฃจํ‹ด ํ•จ์ˆ˜ (๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋“ฑ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
    suspend fun fetchData(): String {
        delay(1000) // 1์ดˆ ์ง€์—ฐ (์‹ค์ œ ์ž‘์—…์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
        return "Success"
    }
}

withTimeout

  • runTest ํ™˜๊ฒฝ ์•ˆ์—์„œ withTimeout์„ ์ ์šฉํ•  ๊ฒฝ์šฐ, ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•˜๋ฉด ํ…Œ์ŠคํŠธ์— ์‹คํŒจํ•˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
@Test
fun `test coroutine with timeout`() = runTest {
    val result = withTimeout(500) { fetchData() }
    assertEquals("Success", result)
}

TestDispachers

  • TestDispatchers๋Š” ํ…Œ์ŠคํŠธ ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” CoroutineDispatcher ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค.
  • ์ƒˆ ์ฝ”๋ฃจํ‹ด์˜ ์‹คํ–‰์„ ์˜ˆ์ธกํ•  ์ˆ˜ ์žˆ๋„๋ก ํ…Œ์ŠคํŠธ ์ค‘์— ์ƒˆ ์ฝ”๋ฃจํ‹ด์„ ๋งŒ๋“œ๋Š” ๊ฒฝ์šฐ์— TestDispathcers๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

https://developer.android.com/kotlin/coroutines/test?hl=ko#testdispatchers

 

Android์—์„œ Kotlin ์ฝ”๋ฃจํ‹ด ํ…Œ์ŠคํŠธ  |  Android Developers

์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Android์—์„œ Kotlin ์ฝ”๋ฃจํ‹ด ํ…Œ์ŠคํŠธ ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถ„๋ฅ˜ํ•˜์„ธ์š”. ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•˜๋Š” ๋‹จ

developer.android.com

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ์ฝ”๋ฃจํ‹ด์ด ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋œ๋‹ค๋ฉด ์˜ˆ์ธก ๊ฐ€๋Šฅ์„ฑ์ด ๋–จ์–ด์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฝ”๋ฃจํ‹ด์˜ ์‹คํ–‰ ์‹œ๊ฐ„, ์‹คํ–‰ ์ˆœ์„œ ๋“ฑ์„ ๋ณด์žฅ๋ฐ›์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. TestDispachers๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ํ•˜๋‚˜์˜ ์Šค์ผ€์ฅด๋Ÿฌ๋ฅผ ๊ณต์œ ํ•˜๊ณ  ์ด ์Šค์ผ€์ฅด๋Ÿฌ์— ๋Œ€ํ•ด ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์Šค๋ ˆ๋“œ๊ฐ€ ๊ณต์œ ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ณ‘๋ชฉ ํ˜„์ƒ์„ ๋ง‰๊ณ , ์‹คํ–‰ ์‹œ๊ฐ„, ์‹คํ–‰ ์ˆœ์„œ์— ๋Œ€ํ•ด ๋ณด์žฅ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

StandardTestDispatcher

  • StandardTestDispatcher์—์„œ ์ƒˆ ์ฝ”๋ฃจํ‹ด์„ ์‹œ์ž‘ํ•˜๋ฉด ์ฝ”๋ฃจํ‹ด์ด ๊ธฐ๋ณธ ์Šค์ผ€์ฅด๋Ÿฌ์˜ ๋Œ€๊ธฐ์—ด์— ์ถ”๊ฐ€๋˜์–ด ํ…Œ์ŠคํŠธ ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ๋•Œ๋งˆ๋‹ค ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  • runTest๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” TestDispatcher์ด๋ฉฐ ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ ์ฝ”๋ฃจํ‹ด ์Šค์ผ€์ฅด๋ง๊ณผ ๋น„์Šทํ•˜๊ฒŒ ๋งž์ถฐ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์„๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
class DataRepositoryTest {

    private lateinit var repository: DataRepository

    // ํ…Œ์ŠคํŠธ ๋””์ŠคํŒจ์ฒ˜ ์„ ์–ธ
    private val testDispatcher = StandardTestDispatcher()

    @Before
    fun setup() {
        // ํ…Œ์ŠคํŠธ ๋””์ŠคํŒจ์ฒ˜๋ฅผ ๋ฉ”์ธ์œผ๋กœ ์„ค์ •
        Dispatchers.setMain(testDispatcher)
        repository = DataRepository()
    }

    @After
    fun tearDown() {
        // ๋ฉ”์ธ ๋””์ŠคํŒจ์ฒ˜๋ฅผ ๊ธฐ๋ณธ ๋””์ŠคํŒจ์ฒ˜๋กœ ๋ณต๊ตฌ
        Dispatchers.resetMain()
    }

    @Test
    fun `test fetchData returns Success`() = runTest {
        // fetchData๊ฐ€ "Success"๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ
        val result = repository.fetchData()
        assertEquals("Success", result)
    }
}

์Šค์ผ€์ค„๋Ÿฌ ์ž‘์—… ์‹คํ–‰

  • StandardTestDispatcher๋ฅผ ํ™œ์šฉํ•œ ์•„๋ž˜ ํ…Œ์ŠคํŠธ์˜ ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ์— ์‹คํŒจํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ ์Šค๋ ˆ๋“œ๋ฅผ ๋ฐ”๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์—†๊ฒŒ ๋˜๋ฉด, ๋Œ€๊ธฐ์—ด์— ์žˆ๋Š” ์ƒํƒœ๊ฐ€ ๋˜๊ณ  assertEquals๊ฐ€ ๋ฐ”๋กœ ํ˜ธ์ถœ๋˜๋ฉด์„œ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.
@Test
fun standardTest() = runTest {
    val userRepo = UserRepository()

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

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // โŒ Fails
}
  • ์Šค์ผ€์ค„๋Ÿฌ์— ์žˆ๋Š” ์ž‘์—…๋“ค์„ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜ api๋ฅผ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • [advanceUntilIdle] : ๋Œ€๊ธฐ์—ด์— ์•„๋ฌด๊ฒƒ๋„ ๋‚จ์•„์žˆ์ง€ ์•Š์„ ๋•Œ๊นŒ์ง€ ์Šค์ผ€์ค„๋Ÿฌ์—์„œ ๋‹ค๋ฅธ ๋ชจ๋“  ์ฝ”๋ฃจํ‹ด์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • [advanceTimeBy] : ์ฃผ์–ด์ง„ ์–‘๋งŒํผ ๊ฐ€์ƒ ์‹œ๊ฐ„์„ ์ง„ํ–‰ํ•˜๊ณ , ๊ฐ€์ƒ ์‹œ๊ฐ„์˜ ํ•ด๋‹น ์ง€์  ์ „์— ์‹คํ–‰๋˜๋„๋ก ์˜ˆ์•ฝ๋œ ์ฝ”๋ฃจํ‹ด์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • [runCurrent] : ํ˜„์žฌ ๊ฐ€์ƒ ์‹œ๊ฐ„์— ์˜ˆ์•ฝ๋œ ์ฝ”๋ฃจํ‹ด์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

์ด์ „ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜์ •ํ•˜๋ ค๋ฉด ์–ด์…œ์„ ์œผ๋กœ ๊ฒ€์‚ฌํ•˜๊ธฐ ์ง์ „์— advanceUntilIdle๋ฅผ ํ™œ์šฉํ•ด ๋Œ€๊ธฐ ์ค‘์ธ ์ฝ”๋ฃจํ‹ด ๋‘ ๊ฐœ๊ฐ€ ์ž‘์—…์„ ์‹คํ–‰ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” launch ํ˜ธ์ถœ์—์„œ ๋ฐ˜ํ™˜๋œ Job ์ธ์Šคํ„ด์Šค๋ฅผ joinํ•˜์—ฌ ์–ด์„ค์…˜์„ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ์ƒˆ ์ฝ”๋ฃจํ‹ด์„ ์™„๋ฃŒํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์ ์€ StandardTestDispatcher๋Š” ๋ณต์žกํ•œ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ์œ ์šฉํ•œ ์ฝ”๋ฃจํ‹ด ์‹คํ–‰์„ ์ •๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

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

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }
    advanceUntilIdle() // Yields to perform the registrations

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // โœ… Passes
}

UnconfinedTestDispatcher

  • UnconfinedTestDispatcher๋Š” Kotlin Coroutine Test ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ํ…Œ์ŠคํŠธ ๋””์ŠคํŒจ์ฒ˜๋กœ, ์ง€์—ฐ ์—†์ด ์ฆ‰์‹œ ์‹คํ–‰๋˜๋Š” ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ์ผ๋ฐ˜์ ์ธ ๋””์ŠคํŒจ์ฒ˜์™€ ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋ฉฐ, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
  • ์–ด๋–ค ํŠน์ • ์Šค๋ ˆ๋“œ์— ๋ฐ”์ธ๋”ฉ๋˜์ง€ ์•Š๊ณ , ๋ฐ”๋กœ ์‹คํ–‰ํ•˜๋ฉฐ ์ฝ”๋ฃจํ‹ด์˜ ๋™์ž‘์„ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ๋™๊ธฐ์ ์œผ๋กœ ์ฆ‰์‹œ ์‹คํ–‰๋˜๋Š” ํ…Œ์ŠคํŠธ์—์„œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
class UnconfinedDispatcherTest {

    // UnconfinedTestDispatcher ์„ ์–ธ
    private val testDispatcher = UnconfinedTestDispatcher()

    private lateinit var repository: DataRepository

    @Before
    fun setup() {
        // ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ Dispatchers.Main์„ UnconfinedTestDispatcher๋กœ ์„ค์ •
        Dispatchers.setMain(testDispatcher)
        repository = DataRepository()
    }

    @After
    fun tearDown() {
        // ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด ๋ฉ”์ธ ๋””์ŠคํŒจ์ฒ˜๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ณต๊ตฌ
        Dispatchers.resetMain()
    }

    @Test
    fun `test fetchData with UnconfinedTestDispatcher`() = runTest {
        // fetchData๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ์ฝ”๋ฃจํ‹ด ํ•จ์ˆ˜
        val result = repository.fetchData()
        assertEquals("Success", result)
    }
}

CoroutinesTestExtension

  • ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ UnconfinedTestDispatcher๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋ณ„๋„์˜ ์ต์Šคํ…์…˜์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
@ExperimentalCoroutinesApi
class CoroutinesTestExtension(
    private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : BeforeEachCallback, AfterEachCallback {

    override fun beforeEach(context: ExtensionContext?) {
        Dispatchers.setMain(dispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        Dispatchers.resetMain()
    }
}

UnconfinedTestDispatcher vs StandardTestDispatcher

ํŠน์ง•UnconfinedTestDispatcherStandardTestDispatcher

์‹คํ–‰ ๋ฐฉ์‹ ์ฆ‰์‹œ ์‹คํ–‰ (์ง€์—ฐ ์—†์Œ) ์˜ˆ์•ฝ๋œ ์ž‘์—…์„ ํ์— ์Œ“์•„ ๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌ
์Šค๋ ˆ๋“œ ๋ฐ”์ธ๋”ฉ ํŠน์ • ์Šค๋ ˆ๋“œ์— ๋ฐ”์ธ๋”ฉ๋˜์ง€ ์•Š์Œ ํ…Œ์ŠคํŠธ ์Šค๋ ˆ๋“œ์—์„œ ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰
์‚ฌ์šฉ ์‹œ๊ธฐ ์ฝ”๋ฃจํ‹ด์„ ์ฆ‰์‹œ ์‹คํ–‰ํ•ด์•ผ ํ•  ๋•Œ ์ฝ”๋ฃจํ‹ด์ด ์‹คํ–‰๋  ์‹œ์ ์„ ์ œ์–ดํ•ด์•ผ ํ•  ๋•Œ
๋น„๋™๊ธฐ ์ž‘์—… ๊ธฐ๋ณธ์ ์œผ๋กœ ๋น„๋™๊ธฐ ์ž‘์—… ์—†์Œ ๋น„๋™๊ธฐ ์ž‘์—…์„ ํ…Œ์ŠคํŠธํ•  ๋•Œ ์‚ฌ์šฉ

์ฐธ๊ณ 

https://velog.io/@picbel/Kotlin-Coroutine-ํ…Œ์ŠคํŠธ-์ฝ”๋“œ-์ž‘์„ฑ-์˜ˆ์ œ

https://munseong.dev/kotlin/runtest/