๐ก์ฐ์ํํ ํฌ์ฝ์ค ํ๋ ์ค์ ๊ฒฝํํ ์ฝ๋ฃจํด ํ ์คํธ์ ๋ํ์ฌ ๊ธฐ๋กํ์์ต๋๋ค.
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
ํ ์คํธ ์ฝ๋์์ ์ฝ๋ฃจํด์ด ์ฌ๋ฌ ์ค๋ ๋์์ ์คํ๋๋ค๋ฉด ์์ธก ๊ฐ๋ฅ์ฑ์ด ๋จ์ด์ง๊ฒ ๋ฉ๋๋ค. ์ฝ๋ฃจํด์ ์คํ ์๊ฐ, ์คํ ์์ ๋ฑ์ ๋ณด์ฅ๋ฐ์ง ๋ชปํฉ๋๋ค. 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-ํ ์คํธ-์ฝ๋-์์ฑ-์์