๐ก ์ฝํ๋ฆฐ ์ฝ๋ฃจํด์์ ํ ์คํธํ๋ ๋ฐฉ๋ฒ์ ๋ํ์ฌ ํ์ตํ์์ต๋๋ค.
์ฝํ๋ฆฐ ์ฝํฌ๋ฆฐ ํ ์คํธ
- ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์ค๋จ ํจ์๋ฅผ ํ ์คํธํ๋ ๊ฒ์ ์ผ๋ฐ์ ์ธ ํจ์๋ฅผ ํ ์คํธํ๋ ๊ฒ๊ณผ ๋ค๋ฅด์ง ์์ต๋๋ค.
class FetchUserUseCase(
private val repo: UserDataRepository
) {
suspend fun fetchUserData(): User = coroutineScope {
val name = async { repo.getName() }
val friends = ascycn { repo.getFriends() }
val profile async { repo.getProfile() }
User(
name = name.await(),
friends = friends.await(),
profile profile.await()
)
}
}
@Test
class FetchUserDataTest{
// given
val repo = FakeUserDataRepository()
val useCase = FetchUserUseCase(repo)
// when
val result = useCase.fetchUserData()
// then
val expectedUser = User(
name = "Ben",
friends = listOf(Friend("friend-id-1:")),
profile = Profile("description")
)
assertEquals(expectedUser, result)
}
class FakeUserDataRepository : UserDataRepository
- ๊ฐ์ง ํด๋์ค์ ๊ฐ๋จํ ์ด์ค์ ์ ์ฌ์ฉํด ์ํ๋ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์์๋์ง ํ์ธํฉ๋๋ค.
- ๋ชฉ ๊ฐ์ฒด ๋์ ๊ฐ์ง ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์์กดํ์ง ์๊ธฐ ์ํจ์ ๋๋ค.
์๊ฐ ์์กด์ฑ ํ ์คํธ (StandardTestDispatcher)
suspend fun produceCurrentUserSeq(): User {
val profile = repo.getProfile()
val friends = repo.getFriends()
return User(profie, friends)
} // ์์ฐจ์ ์์ฑ
suspend fun produceCurrentUserSym(): User = coroutineScope {
val profile = async { repo.getProfile() }
val friends = async { repo.getFriends() }
User(profile.await(), firends.await())
} // ๋์์ ์์ฑ
- ์ฒซ ๋ฒ์งธ ํจ์๋ 2์ด, ๋๋ฒ์งธ ํจ์๋ 1์ด๊ฐ ๊ฑธ๋ฆฌ๊ฒ ๋ฉ๋๋ค.
- ๋ ํจ์์ ์ฐจ์ด๋ฅผ ์ด๋ป๊ฒ ํ ์คํธํ ์ง ์๊ฐํด๋ณผ ์ ์์ต๋๋ค.
class FakeDelayedUserDataRepository : UserDataRepository // ๋๋ ์ด๋ฅผ ์ฃผ๋ ํจ์
- ์ค์ ๋ก ํ ์คํธ์ ๋๋ ์ด๋ฅผ ์ค์ ์๊ฐ์ ์ค์ผ ์ ์์ง๋ง, ํ ์คํธ ์ ์ฒด๋ฅผ ์ํํ๋ ์๊ฐ์ ์ฆ๊ฐ์ํค๊ฒ ๋ฉ๋๋ค.
- ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ kotlinx-coroutines-test ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณตํ๋ StandardTestDispatcher๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
TestCoroutineScheduler์ StandardTestDispatcher
- TestCoroutineScheduler๋ delay๋ฅผ ๊ฐ์ ์๊ฐ ๋์ ์คํํ์ฌ ์ค์ ์๊ฐ์ด ํ๋ฌ๊ฐ ์ํฉ๊ณผ ๋์ผํ๊ฒ ์๋ํ๊ธฐ ๋๋ฌธ์ ์ ํด์ง ์๊ฐ๋งํผ ๊ธฐ๋ค๋ฆฌ์ง ์๋๋ก ๋ณ๊ฒฝํ ์ ์์ต๋๋ค.
- StandardTestDispatcher๋ ๋ค๋ฅธ ๋์คํจ์ฒ์ ๋ฌ๋ฆฌ ์ฝ๋ฃจํด์ด ์คํ๋์ด์ผ ํ ์ค๋ ๋๋ฅผ ๊ฒฐ์ ํ ๋๋ง ์ฌ์ฉ๋๋ ๊ฒ์ ์๋๋ฉฐ, ํ ์คํธ ๋์คํจ์ฒ๋ก ์์๋ ์ฝ๋ฃจํด์ ๊ฐ์ ์๊ฐ๋งํผ ์งํ๋๊ธฐ ์ ๊น์ง ์คํ๋์ง ์์ต๋๋ค.
- ์ค์ ์๊ฐ์ฒ๋ผ ์๋ํ๋ ๊ฐ์ ์๊ฐ์ด ํ๋ฅด๊ฒ ํ์ฌ, ๊ทธ ์๊ฐ ๋์ ํธ์ถ๋ ๋ชจ๋ ์์ ์ ์คํํ๋ advanceUntilIdle์ ์ฌ์ฉํฉ๋๋ค.
fun main() {
val scheduler = TestCoroutineScheduler()
val testDispatcher = StandardTestDispatcher(scheduler)
CoroutineScope(testDispatcher).launch {
println("task1")
delay(1000)
println("task2")
delay(1000)
println("done")
}
println() // scheduer.currentTime Before
scheduler.advanceUntilIdle()
println() // scheduer.currentTime After
}
// [0] Before
// task1
// task2
// done
// [2000] After
- ๊ธฐ๋ณธ์ ์ผ๋ก StandardTestDispatcher๋ TestCoroutineScheduler๋ฅผ ๋ง๋ค๊ธฐ ๋๋ฌธ์ ๋ช ์์ ์ผ๋ก ๋ง๋ค์ง ์์๋ ๋ฉ๋๋ค.
- StandardTestDispatcher๋ ์ง์ ์๊ฐ์ ํ๋ฅด๊ฒ ํ์ง ์์ผ๋ฉฐ, advanceUntilIdle๋ฅผ ์คํํด์ StandardTestDispatcher๋ฅผ ๋ช
์ํ์ง ์์ผ๋ฉด ์ฝ๋ฃจํด์ด ์ฌ๊ฐ๋์ง ์์ต๋๋ค.
- ์ด๋ ์ฝ๋๊ฐ ์์ํ ์คํ๋จ์ ์๋ฏธํฉ๋๋ค.
advanceTimeBy
- advanceUntilIdle์ฒ๋ผ ์๊ฐ์ ํ๋ฅด๊ฒํ๋ ๋๋ค๋ฅธ ๋ฐฉ๋ฒ์ด๋ฉฐ, advanceTimeBy์ ์ผ์ ๋ฐ๋ฆฌ์ด๋ฅผ ์ธ์๋ก ๋ฃ์ด ํ์ฉํ ์ ์์ต๋๋ค.
- advanceTimeBy๋ ์๊ฐ์ ํ๋ฅด๊ฒ ํ๊ณ ๊ทธ๋์ ์ผ์ด๋ฌ์ ๋ชจ๋ ์ฐ์ฐ์ ์ํํฉ๋๋ค.
- runCurrent ํจ์๋ฅผ ์ถ๊ฐ๋ก ์ฌ์ฉํ์ฌ ์ ํํ ์ผ์นํ๋ ์๊ฐ์ ์์ ๋ ์ฐ์ฐ์ ์ฌ๊ฐํ ์ ์์ต๋๋ค.
val testDispatcher = StandardTestDispatcher()
CoroutineScope(testDispatcher).launch // task1
CoroutineScope(testDispatcher).launch // task2
testDispatcher.scheduler.advanceTimeBy(2) // task1 ์ํ
testDispatcher.scheduler.runCurrent() // task2 ์ํ
๊ฐ์ ์๊ฐ์ด ์๋ํ๋ ๋ฐฉ๋ฒ
- delay๊ฐ ํธ์ถ๋๋ฉด ๋์คํจ์ฒ๊ฐ Delay ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋์ง ํ์ธํ๊ณ , ์ค์ ์๊ฐ๋งํผ ๊ธฐ๋ค๋ฆฌ๋ DefaultDelay ๋์ ๋์คํจ์ฒ๊ฐ ๊ฐ์ง scheduleResumeAfterDelay ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
- ๊ฐ์ ์๊ฐ์ ์ค์ ์๊ฐ๊ณผ ๋ฌด๊ดํ๊ฒ ๋์ํ๋ฉฐ, Thread.sleep์ StandardTestDispatcher์ ์ฝ๋ฃจํด์ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.
advanceUntilIdle
- ๋ช ๋ฐ๋ฆฌ์ด ์ด๋ด์ ๊ฐ์ ์๊ฐ๋ง ํ๋ฅด๊ฒํ์ฌ ์ฝ๋ฃจํด ์ฐ์ฐ์ ์คํ ๊ฐ๋ฅํ๊ฒ ํ๋ ํจ์์ ๋๋ค.
val dispatcher = StandardTestDispathcer()
CoroutineScope(dispatcher).launch {
delay(1000)
println("Done")
}
Thread.sleep(Random.nextLong(2000))
// ์ฌ๊ธฐ์ ์ผ๋ง๋ ๊ธฐ๋ค๋ฆฌ๋์ง ์๊ด ์์ด ๊ฒฐ๊ณผ์ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.
println("${dispatcher.scheduler.currentTime} Before")
dispatcher.scheduler.advanceUntilIdle() // advanceUntilIdleํ์ฉ
println("${dispatcher.scheduler.currentTime} After")
// [0] Before
// Done
// [1000] After
TestScope
- ์ค์ฝํ๋ง์ผ๋ก ํด๋น ํจ์์ ํ๋กํผํฐ๋ฅผ ์ฌ์ฉํ ์ ์๋ ์ค์ฝํ์ ๋๋ค.
- StandardTestDispatcher์ ๊ฐ์ ์ญํ ์ ์ํํ๋ฉฐ, CoroutineExceptionHandler๋ก ๋ชจ๋ ์์ธ๋ฅผ ๋ชจ์ ์ ์์ต๋๋ค.
val scope = TestScope()
scope.launch {
delay(1000)
}
scope.advanceTimeBy(1000)
scope.runCurrent()
scope.advanceUntilIdle()
// StandardTestDispatcher์ ํจ์๋ฅผ ๋๊ฐ์ด ์ฌ์ฉํ ์ ์์ต๋๋ค
- ์๋๋ก์ด๋ ๋ทฐ ๋ชจ๋ธ, ํ๋ ์ ํฐ, ํ๋๊ทธ๋จผํธ๋ฅผ ํ ์คํธํ ๋ StandardTestDispatcher๋ฅผ ์์ฃผ ์ฌ์ฉํฉ๋๋ค.
- ํ
์คํธ ๋์คํจ์ฒ๋ฅผ ์ฌ์ฉํด produceCurrentUseSeq์ produceCurrentUserSym ํจ์๋ฅผ ์ฝ๋ฃจํด์์ ์์ํ๊ณ , ์ ํด ์ํ๊ฐ ๋ ๋๊น์ง ์๊ฐ์ ํ๋ผ๊ฒํ ์ ์์ต๋๋ค.
- [์ ํด ์ํ] : ์ปดํจํฐ ์์คํ ์ด ์ฌ์ฉ ๊ฐ๋ฅํ ์ํ์ด๋ ์ค์ ์ ์ธ ์์ ์ด ์๋ ์๊ฐ
- ์ผ๋ง๋ ์๊ฐ์ด ๋ง์ด ํ๋ ๋์ง ํ์ธํ๋ ๋ฐฉ์์ผ๋ก ํ ์คํธํ ์ ์์ต๋๋ค.
- ํ์ง๋ง ์ด๋ฌํ ์ค๊ณ๋ ๋ณต์กํ๊ธฐ ๋๋ฌธ์ runTest๋ฅผ ์ฃผ๋ก ํ์ฉํฉ๋๋ค.
runTest
- kotlinx-coroutines-test์ ํจ์๋ค ์ค ๊ฐ์ฅ ํํ๊ฒ ์ฌ์ฉ๋ฉ๋๋ค.
- TestScope์์ ์ฝ๋ฃจํด์ ์์ํ๊ณ ์ฆ์ ์ ํด ์ํ๊ฐ ๋ ๋๊น์ง ์๊ฐ์ ํ๋ฅด๊ฒ ํ ์ ์์ต๋๋ค.
- TestScope + advanceUntilIdle()๊ฐ ํฉ์ณ์ง ๊ธฐ๋ฅ์ํ๋ฉฐ, runBlocking()๊ณผ ๊ฐ์ ๋์์ ํฉ๋๋ค.
- ์ฝ๋ฃจํด์์ ์๊ฐ์ด ์ผ๋ง๋ ํ๋ ๋์ง ํ์ธํ ์ ์์ผ๋ฉฐ, ํ ์คํธํ๋ ๋ฐ ๋ช ๋ฐ๋ฆฌ์ด๋ง ์๋ชจํฉ๋๋ค.
class Test{
@Test
fun test1() = runTest {
assertEquals(0, currentTime)
coroutineScope {
launch { delay(1000) }
launch { delay(1500) }
launch { delay(2000) }
}
assertEquals(2000, currentTime)
}
}
- runTest๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ์ง ์ ์ฅ์์ ๊ฐ ํจ์๋ฅผ ๋งค์ฐ ๋น ๋ฅด๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
- ๋ํ ๊ฐ์ ์๊ฐ์ ํ์ฉํ๊ธฐ ๋๋ฌธ์ ํ ์คํธ๋ ์ฆ์ ๋๋๊ฒ ๋๋ฉฐ, ์ ํํ currentTime ๊ฐ์ ์ป์ ์ ์์ต๋๋ค.
@Test
fun `Should produce user sequentially`() = runTest {
// given
val userDataRepository = FakeDelayUserDataRepository()
val userCase = ProduceUserUseCase(userDataRepository)
// when
useCase.produceCurrentUserSeq()
//then
assertEquals(2000, currentTime)
}
- runTest์ ๋ค๋ฅธ ์๊ฐ ์์กด์ฑ ํ ์คํธ์์ ๊ด๊ณ๋ ์๋์ ๊ฐ์ต๋๋ค.
๋ฐฑ๊ทธ๋ผ์ด๋ ์ค์ฝํ
- runTest ํจ์๋ ๋ค๋ฅธ ํจ์์ฒ๋ผ ์ค์ฝํ๋ฅผ ๋ง๋ค๋ฉฐ, ์์ ์ฝ๋ฃจํด์ด ๋๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฝ๋๋ค.
- ์ ๋ ๋๋์ง ์๋ ํ๋ก์ธ์ค๋ฅผ ์์ํ๋ค๋ฉด ํ ์คํธ ๋ํ ์ข ๋ฃ๋์ง ์์ต๋๋ค.
@Test
fun `should increment counter`() = runTest {
var i = 0
launch {
while (true) {
delay(1000)
i++
}
}
delay(1001)
assertEquals(1,i)
delay(1000)
assertEquals(2,i)
// coroutineContext.job.cancelChilderen()์ ์ถ๊ฐํ๋ฉด ํ
์คํธ๊ฐ ํต๊ณผํฉ๋๋ค.
// ํ์ฌ ์ฝ๋๋ก๋ ํ
์คํธ๊ฐ ์ข
๋ฃ๋์ง ์์ต๋๋ค...
}
backgroundScope
- ์ด๋ฐ ๊ฒฝ์ฐ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ runTest์์๋ backgroundScope๋ฅผ ์ ๊ณตํฉ๋๋ค.
- ์ด ๋ํ ๊ฐ์ ์๊ฐ์ ์ง์ํ์ง๋ง, runTest๊ฐ ์ค์ฝํ ์ข ๋ฃ๋ฅผ ๊ธฐ๋ค๋ฆฌ์ง ์์ต๋๋ค.
backgroundScope.launch {
// ์ ์ฝ๋์ ๋์ผํ ๋ด์ฉ์
๋๋ค
}
์ทจ์์ ์ปจํ ์คํธ ์ ๋ฌ ํ ์คํธ
- ํน์ ํจ์๊ฐ ๊ตฌ์กฐํ๋ ๋์์ฑ์ ์งํค๋์ง ํ
์คํธํ๋ ค๋ฉด ์๋์ ๊ฐ์ ๊ฐ์ฅ ์ฌ์ด ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
- ์ค๋จ ํจ์๋ก๋ถํฐ ์ปจํ ์คํธ๋ฅผ ๋ฐ์ ๋ค ์ปจํ ์คํธ๊ฐ ๊ธฐ๋ํ ๊ฐ์ ๊ฐ์ง๊ณ ์๋์ง ํ์ธ
- ์ค๋จ ํจ์๋ก๋ถํฐ ์ปจํ ์คํธ๋ฅผ ๋ฐ์ ๋ค ์ก์ด ์ ์ ํ ์ํ์ธ์ง ํ์ธ
suspend fun <T, R> Iterable<T>.mapAsync(
transformation: suspend (T) -> R
): List<R> = coroutineScope {
this@mapAsync.map { async { transformation(it) } }
.awaitAll()
}
- ์ ํจ์๋ ์์๋ฅผ ๋ณด์ฅํ๋ฉด์ ๋น๋๊ธฐ์ ์ผ๋ก ์์๋ฅผ ๋งคํํฉ๋๋ค.
- ์๋์ ๊ฐ์ด ํ ์คํธํด๋ณผ ์ ์์ต๋๋ค.
@Test
fun `sould map async and keep elements order`() = runTest {
val transforms = listOf(
suspend { delay(3000); "A" } ,
suspend { delay(2000); "B" } ,
suspend { delay(4000); "C" } ,
suspend { delay(1000); "D" } ,
)
val res = transforms.mapAsync { it() }
assertEquals(listOf("A","B","C","D"), res)
assertEquals(4000, currentTime)
}
- ์ ํ
์คํธ๋ ๊ตฌ์กฐํ๋ ๋์์ฑ์ ์งํค๋ ์ค๋จ ํจ์๊ฐ ์ ํํ๊ฒ ๊ตฌํ๋์๋ค๊ณ ๋ณด๊ธฐ๋ ์ด๋ ค์ฐ๋ฉฐ ์ด์ ๋ ์๋์ ๊ฐ์ต๋๋ค.
- ๋ถ๋ชจ ์ฝ๋ฃจํด๊ณผ ๊ฐ์ ์ปจํ ์คํธ๋ฅผ ์ฌ์ฉํ๋์ง ํ์ธํ์ง ์์
- ์ทจ์ ์ํฉ์ ํ์ธํ์ง ์์
๋ถ๋ชจ ์ฝ๋ฃจํด๊ณผ ๊ฐ์ ์ปจํ ์คํธ๋ฅผ ์ฌ์ฉํ๋์ง ํ ์คํธ
- ๋ถ๋ชจ ์ฝ๋ฃจํด์์ CoroutineName๊ณผ ๊ฐ์ ํน์ ์ปจํ ์คํธ๋ฅผ ๋ช ์ํ์ฌ transformation ํจ์์์ ๊ทธ๋๋ก์ธ์ง ํ์ธํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ฉํฉ๋๋ค.
- ์ค๋จ ํจ์์์ ์ปจํ ์คํธ๋ฅผ ํ์ธํ๋ ค๋ฉด currentCoroutineContext ํจ์๋ coroutineContext ํ๋กํผํฐ๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
// in runTest
val ctx: CoroutineContext? = null
val name = CoroutineName("Name")
withContext(name) {
listOf("A").mapAsync {
ctx = currentCoroutineContext()
it
}
assertEquals(name, ctx?.get(CoroutineName))
}
์ทจ์ ์ํฉ์ ํ์ธ
- ์ทจ์๋ฅผ ํ์ธํ๋ ๋ฐฉ๋ฒ์ ์๋์ ์์๋ก ์งํ๋ฉ๋๋ค.
- ๋ด๋ถ ํจ์์์ ์ก์ ์ฐธ์กฐํฉ๋๋ค.
- ์ธ๋ถ ์ฝ๋ฃจํด์์ ์ฝ๋ฃจํด์ ์ทจ์ํฉ๋๋ค.
- ์ฐธ์กฐ๋ ์ก์ด ์ทจ์๋ ๊ฒ์ ํ์ธํฉ๋๋ค.
// in runTest
val job: Job? = null
val parentJob = launch { // ๋ด๋ถ ํจ์์์ ์ก ์ฐธ์กฐ
listOf("A").mapAsync {
job = currentCoroutineContext().job
delay(Long.MAX_VALUE)
}
}
delay(1000)
parentJob.cancel() // ์ธ๋ถ ์ฝ๋ฃจํด ์ทจ์
assertEquals(true, job?.isCancelled) // ์ฐธ์กฐ๋ ์ก์ด ์ทจ์๋ ๊ฒ์ ํ์ธ
ํ ์คํธ์ ํต๊ณผํ์ง ๋ชปํ๋ ๊ฒฝ์ฐ
- ๋๋ถ๋ถ์ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ์์ ๊ฐ์ ํ ์คํธ๊ฐ ํ์ํ์ง ์์ง๋ง, ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์๋ ํ์ฉ๋ฉ๋๋ค.
- ๋ง์ฝ async๋ฅผ ์ธ๋ถ ์ค์ฝํ์์ ์คํํ๋ค๋ฉด ์ ํ
์คํธ์์๋ ์คํจํฉ๋๋ค.
- ๊ตฌ์กฐํ๋ ๋์์ฑ์ ์งํค์ง ๋ชปํ๋ ๊ฒฝ์ฐ
suspend fun <T,R> Iterable<T>.mapAsync(
transformation: suspend (T) -> R
): List<R> =
this@mapAsync
.map { GlobalScope.async { transformation(it) } }
.awaitAll()
UnconfinedTestDisptcher
- StandardTestDispatcher์ ๋น์ทํ ๊ธฐ๋ฅ์ ํ๋ UnconfinedTestDisptcher ๋์คํจ์ฒ๊ฐ ์์ต๋๋ค.
- StandardTestDispatcher์์ ์ฐจ์ด์ ์ StandardTestDispatcher๋ ์ค์ผ์ฅด๋ฌ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ ๊น์ง ์ด๋ค ์ฐ์ฐ๋ ์ํํ์ง ์์ต๋๋ค.
- UnconfinedTestDisptcher๋ ์ฝ๋ฃจํด์ ์์ํ์ ๋ ์ฒซ ๋ฒ์งธ ์ง์ฐ์ด ์ผ์ด๋๊ธฐ ์ ๊น์ง ๋ชจ๋ ์ฐ์ฐ์ ์ฆ์ ์ํํฉ๋๋ค.
fun main() {
CoroutineScope(StandardTestDispatcher()).launch {
print("A")
delay(1)
print("B")
}
CoroutineScope(UnconfinedTestDisptcher()).launch {
print("C")
delay(1)
print("D")
}
}
// ์ฆ์ ์ํํ๋ฏ๋ก, C๋ฅผ ๊ฐ์ฅ ๋จผ์ ์ถ๋ ฅ
// C
- ์๋์ ๊ฐ์ด runTest์ ์ด์ํด์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
@Test
fun testName() = runTest(UnconfinedTestDisptcher())
๋ชฉ(mock) ์ฌ์ฉํ๊ธฐ
- ๋ชจํน ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ๋ฉด ๋ชจ๋ ํ ์คํธ๊ฐ ์ฌ์ฉํ๋ ์ ์ฅ์์ ์ธํฐํ์ด์ค๋ฅผ ๋ฐ๊พธ๋ ์ํฉ์ ํผํ ์ ์๊ณ , ์ผ๋ถ ํด๋์ค๋ง ์์ ํ์ฌ ์ด๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
@Test
fun `should load data concurrently`() = runTest {
//given
val userRepo = mockk<UserDataRepository>()
coEvery { userRepo.getName() } coAnswers {
delay(600)
someFriends
}
val useCase = FetchUserUseCase(userRepo)
// when
useCase.fetchUserData()
// then
assertEquals(800, currentTime)
}
๋์คํจ์ฒ๋ฅผ ๋ฐ๊พธ๋ ํจ์ ํ ์คํธ
- ์ผ๋ฐ์ ์ผ๋ก ๋ธ๋กํน ํธ์ถ์ด ๋ง๋ค๋ฉด Dispatchers.IO(or ์ปค์คํ ๋์คํจ์ฒ)๋ฅผ ํธ์ถํ๊ณ , CPU ์ก์ฝ์ ์ธ ํธ์ถ์ด ๋ง๋ค๋ฉด Dispatchers.Default๋ฅผ ํธ์ถํฉ๋๋ค.
- ๋ ๊ฒฝ์ฐ ๋ชจ๋ ๋์์ ์คํ๋ ํ์๊ฐ ๊ฑฐ์ ์๊ณ runBlocking์ ์ฌ์ฉํด ํ ์คํธํ ์ ์์ต๋๋ค.
- ํ์ง๋ง ํจ์๊ฐ ์ค์ ๋ก ๋์คํจ์ฒ๋ฅผ ๋ฐ๊พธ๋ ๊ฒฝ์ฐ๋ ์ด๋ป๊ฒ ํ์ธํ ์ ์์์ง ์๊ฐํด๋ด์ผ ํฉ๋๋ค.
- ํธ์ถํ๋ ํจ์๋ฅผ ๋ชจํนํ์ฌ, ์ฌ์ฉํ ์ค๋ ๋์ ์ด๋ฆ์ ๊ฐ์ง๊ณ ์ค๋ ๋ฐฉ๋ฒ์ผ๋ก ํ์ธํ ์ ์์ต๋๋ค.
@Test
fun `should change dispatcher`() = runBlocking {
// given
val csvReader = mockk<CsvReader>()
val startThreadName = "MyName"
val userThreadName: String? = null
every {
scvReader.readCsvBlocking(
aFileName,
GameState::class.java
)
} coAnswers {
usedThreadName = Thread.currentThread().name
aGameState
}
val saveReader = SaveReader(scvReader)
//when
withContext(newSingleThreadContext(startThreadName)) {
saveReader.readSvae(aFileName)
}
//then
assertNotNull(usedThreadName)
val expectedPrefix = "DefaultDispatcher-worker-"
assert(usedThreadName!!.startsWith(expetedPrefix))
}
ํจ์ ์คํ ์ค์ ์ผ์ด๋๋ ์ผ ํ ์คํธ
- ์๋์ ๊ฐ์ด ์คํ ์ค์ ํ๋ก๊ทธ๋ ์ค ๋ฐ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ์จ๊ธฐ๋ ํจ์๋ฅผ ์์ฑํ์์ต๋๋ค.
suspend fun sendUserData() {
val userData = database.getUserData()
progressBarVisible.value = true
userRepository.sendUserData(userData)
progressBarVisible.value = false
}
- ์ต์ข ๊ฒฐ๊ณผ๋ ๊ฒฐ๊ตญ false ๋ก ์ ํ๋๋ฏ๋ก, ์ค์ ํ๋ก๊ทธ๋ ์ค ๋ฐ์ ์ํ๊ฐ ๋ณ๊ฒฝ๋์๋์ง ํ์ธํ ์ ์์ต๋๋ค.
- ์ด๋ฐ ๊ฒฝ์ฐ ํจ์๋ฅผ ์๋ก์ด ์ฝ๋ฃจํด์์ ์์ํ๊ณ ๋ฐ๊นฅ์์ ๊ฐ์ ์๊ฐ์ ์กฐ์ ํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ฉํฉ๋๋ค.
runTest ํ์ฉ
- runTest๋ ์ฝ๋ฃจํด์ ๋์คํจ์ฒ๋ก StandardTestDispatcher๋ฅผ ์ง์ ํ๋ฉฐ ๋๊ธฐ ์ํ๊ฐ ๋ ๋๊น์ง ์๊ฐ์ด ํ๋ฅด๊ฒ ํฉ๋๋ค.
- advanceUntilIdle ํจ์๋ฅผ ์ฌ์ฉ
- ์์ ์ฝ๋ฃจํด์ ์๊ฐ์ ๋ถ๋ชจ๊ฐ ์์์ ๊ธฐ๋ค๋ฆฌ๊ธฐ ์์ํ์ ๋ (ํจ์ ๋ณธ์ฒด์ ์คํ์ ๋๋์ ๋) ํ๋ฅด๊ฒ ๋ฉ๋๋ค.
- ๊ทธ ์ด์ ์๋ ๊ฐ์ ์๊ฐ์ ์กฐ์ ํ ์ ์์
@Test
fun `should show progress bar when sending data`() =
runTest {
val database = FakeDatabase()
val vm = UserViewModel(database)
launch {
vm.showUserData()
}
// then
assertEquals(false, vm.progressBarVisible.valule)
delay(1000)
assertEquals(true, vm.progressBarVisible.valule)
delay(1000)
assertEquals(false, vm.progressBarVisible.valule)
}
- ์ด ๋ delay๋ณด๋ค๋ advanceTimeBy์ ๊ฐ์ ๋ช ์์ ์ธ ํจ์๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด ๊ฐ๋ ์ฑ์ด ์ข์ ์ ์์ต๋๋ค.
์๋ก์ด ์ฝ๋ฃจํด์ ์์ํ๋ ํจ์ ํ ์คํธ
- ๋ฐฑ์๋์์๋ ํ๋ ์์ํฌ์์ ์ฃผ๋ก ์ฝ๋ฃจํด์ด ์์๋์ง๋ง, ์ง์ ๋ง๋ ์ค์ฝํ ๋ด๋ถ์์ ์ฝ๋ฃจํด์ ์์ํ ๋๋ ์์ต๋๋ค.
@Scheduled(fixedRate = 5000)
fun sendNotifications() {
notificationsScope.launch {
val notifications = notificationsRepository
.notificationsToSend()
for (notification in notifications) {
launch {
notificationService.send(notification)
notificationRepository
.markAsSent(notification.id)
}
}
}
- sendNotifications() ํจ์์์ ์๋ฆผ์ด ์ค์ ๋ก ๋์์ ์ ์กํ๋์ง ํ ์คํธํ๊ณ ์ ํฉ๋๋ค.
- ๋จ์ ํ
์คํธ์์ ์ค์ฝํ์ ์ผ๋ถ๋ก StandardTestDispatcher๋ฅผ ์ฌ์ฉํ ์ ์์ผ๋ฉฐ, send์ markAsSent ์ฌ์ด์ ์ฝ๊ฐ์ ์ง์ฐ์ด ์๊ธฐ๋๋ก ์ํํ์์ต๋๋ค.
- advanceUntilIdle, sendNotifications์ ์ผ๋ฐ ํจ์์ด๊ธฐ ๋๋ฌธ์ runBlocking์ ์ฌ์ฉํ์ง ์์์ต๋๋ค!
@Test
fun testSendNotifications() {
// given
val notifications = List(100) { Notification(it) }
val repo = FakeNotificationsRepository(
delayMillis = 200,
notifications = notifications
)
val service = FakeNotifictaionsService(
delayMillis = 300,
)
val testScope = TestScope()
val sender = NotificationSender(
notificationsRepository = repo,
notificationService = service,
notificationScope = testScope
)
// when
sender.sendNotifications()
testScope.advanceUntilIdle()
// ๋ชจ๋ ์๋ฆผ์ด ๋ณด๋ด์ง๊ณ ์ ์ก ์๋ฃ ํ์
assertEquals(
notifications.toSet(),
service.notificationsSent.toSet()
)
assertEquals(
notifications.map { it.id }.toSet(),
repo.notificationsMarkedAsSent.toSet()
)
// ์๋ฆผ์ด ๋ณ๋ ฌ๋ก ์ ์ก
assertEquals(700, testScope.currentTime)
}
๋ฉ์ธ ๋์คํจ์ฒ ๊ต์ฒด
- ๋จ์ ํ ์คํธ์๋ ๋ฉ์ธ ํจ์๊ฐ ์์ผ๋ฏ๋ก ๋ฉ์ธ ํจ์๋ฅผ ์ฌ์ฉํ๋ ค ํ๋ฉด, ํ ์คํธ์์ ๋ฉ์ธ ๋์คํจ์ฒ๋ฅผ ๊ฐ์ง ๋ชจ๋์ด ์๋ค๋ ์์ธ๋ฅผ ๋์ง๋๋ค.
- ๋งค๋ฒ ๋ฉ์ธ ์ค๋ ๋๋ฅผ ์ฃผ์ ํ๋ ๊ฑด ๋น์ฉ์ด ๋ง์ด ๋ค๊ธฐ ๋๋ฌธ์ kotlinx-coroutines-test ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ Dispatchers์ setMain ํ์ฅ ํจ์๋ฅผ ์ ๊ณตํฉ๋๋ค.
- ๋ชจ๋ ๋จ์ ํ ์คํธ์ ์ํด ํ์ฅ๋๋ ๊ธฐ๋ณธ ํด๋์ค์ setup ํจ์(@Before or @BeforEach๊ฐ ๋ถ์ ํจ์)์์ ๋ฉ์ธ ํจ์๋ฅผ ์ค์ ํด์ผํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
- ์ด๋ฌํ ๊ฒฝ์ฐ ์ฝ๋ฃจํด์ด Dispatchers.Main์์ ํญ์ ์คํ๋๋ค๋ ๊ฒ์ด ๋ณด์ฅ๋ฉ๋๋ค.
@Test
fun settingMainDispatcher() = runTest {
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
Dispatchers.setMain(testDispatcher)
}
์ฝ๋ฃจํด์ ์์ํ๋ ์๋๋ก์ด๋ ํจ์ ํ ์คํธ
- ์๋๋ก์ด๋์ ๊ฒฝ์ฐ ์ฝ๋ฃจํด์ด ViewModel, Presenters, Fragments, Activities์์ ์ฃผ๋ก ์์๋ฉ๋๋ค.
- ์ด ํด๋์ค๋ค์ ๋งค์ฐ ์ค์ํ๋ฏ๋ก ํ
์คํธ๋ฅผ ๋ฐ๋์ ์ํํด์ผ ํฉ๋๋ค.
- ์ด๋ค ์ค์ฝํ๋ฅผ ์ฌ์ฉํ๋์ง๋ ์ค์ํ์ง ์์ผ๋ฉฐ, ์ฝ๋ฃจํด์ ์์ํ๋ ๋ชจ๋ ํด๋์ค์์ ํ๋ ๊ฒ์ฒ๋ผ ์ค์ฝํ๋ฅผ StandardTestDispathcer๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ์์กด์ฑ์ ์ฃผ์ ํ ํ์ ์์ด, Dispatchers.setMain ํจ์๋ฅผ ์ฌ์ฉํด์ ํธํ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค.
private val testDispatcher = StandardTestDispathcer()
@Before
fun setUp() {
testDispatcher = StandardTestDispathcer()
Dispatchers.setMain(testDispatcher)
}
@After
fun down() {
DiDispatchers.resetMain()
}
- ์์ ๊ฐ์ด ๋์คํจ์ฒ๋ฅผ ์์ฑํ๋ฉด onCreate ์ฝ๋ฃจํด์ testDispatcher์์ ์คํ๋๋ฏ๋ก ์๊ฐ์ ์กฐ์ํ๋ ๊ฒ์ด ๊ฐ๋ฅํฉ๋๋ค.
- ์ด ๋ advanceUntilIdle()๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
private lateinit var scsheduler : TestCorouitneScheduler
@BeforeEach
fun setUp() {
scheduler = TestCoroutineScheduler()
}
@Test
fun `should show user name and sorteed news`() {
//when
viewModel.onCreate()
scheduler.advanceUntilIdle()
}
๋ฃฐ์ด ์กด์ฌํ๋ ํ ์คํธ ๋์คํจ์ฒ ์ค์
- JUnit4๋ ๋ฃฐ ํด๋์ค์ ์ฌ์ฉ์ ํ์ฉํฉ๋๋ค.
๋ฃฐ
- ํ ์คํธ ํด๋์ค์ ์๋ช ๋์ ๋ฐ๋์ ์คํ๋์ด์ผ ํ ๋ก์ง์ ํฌํจํ๋ ํด๋์ค
- ๋ชจ๋ ํ ์คํธ๊ฐ ์์๋๊ธฐ ์ ๊ณผ ๋๋ ๋ค์ ์คํํด์ผ ํ ๊ฒ๋ค์ ์ ์ํ ์ ์์๋๋ค.
- ํ ์คํธ ๋์คํจ์ฒ๋ฅผ ์ค์ ํ๊ณ ๋์ค์ ์ด๋ฅผ ํด์ ํด์ผ ํฉ๋๋ค.
๋ฃฐ ๊ตฌํ
- starting()๊ณผ finished()์ ๊ฐ์ ์๋ช ์ฃผ๊ธฐ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ TestWatcher๋ฅผ ํ์ฅํ ์ ์์ต๋๋ค.
- ๋ฃฐ์ TestCoroutineScheduler์ TestDispatcher๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.
- ๋ฃฐ์ ์ฌ์ฉํ ํด๋์ค์ ๊ฐ ํ ์คํธ๊ฐ ์์๋๊ธฐ ์ ์ TestDispatcher๊ฐ ๋ฉ์ธ ๋์คํจ์ฒ๋ก ์ค์ ๋๋ฉฐ, ๊ฐ ํ ์คํธ๊ฐ ๋๋ ๋ค์ ๋ฉ์ธ ๋์คํจ์ฒ๋ ์๋ ์ํ๋ก ๋์์ค๊ฒ ๋ฉ๋๋ค.
- ์ค์ผ์ค๋ฌ๋ ์ ๋ฃฐ์ scheduler ํ๋กํผํฐ๋ก ์ ๊ทผํ ์ ์์ต๋๋ค.
Junit4์ ๋ฃฐ
class MainCoroutineRule : TestWathcer() {
lateinit var scheduler: TestCoroutineScheduler
private set
lateinit var dispatcher: TestDispatcher
private set
override fun starting(description: Description) {
scheduler = TestCoroutineScheduler()
dispatcher = StandardTestDispatcher(scheduler)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
๋ฃฐ ์ฌ์ฉ
class MainViewModelTests {
@get: Rule
var mainCorotineRule = MainCoroutineRule()
// ...
@Test
fun `should show user name and sorted news`() {
// when
viewModel.onCreate()
mainCorotuineRule.scheduler.advanceUntilIdle()
// then
assertEquals(aName, viewModel.userName.value)
val someNewsSorted =
listOf(News(data1), News(data2), News(data3))
assertEquals(someNewSorted, viewModel,news.value)
}
}
- MainCoroutineRule์์ advanceUntilIdle, advanceTimeBy, runCurrent, currentTime์ ์ง์ ํธ์ถํ๊ณ ์ถ๋ค๋ฉด, ํ์ฅ ํจ์์ ํ๋กํฐํฐ๋ก ์ ์ํ ์ ์์ต๋๋ค.
- ์ด๋ฌํ ๋ฐฉ์์ ์๋๋ก์ด๋์์ ์ฝํ๋ฆฐ ์ฝ๋ฃจํด์ ํ ์คํธํ ๋ ์์ฃผ ์ฌ์ฉํ๋ฉฐ, JUnit5์ ๋ฐฉ์์์๋ ํ์ฅ ํด๋์ค๋ฅผ ์ ์ํ๋ค๋ ์ ์์ ํฐ ์ฐจ์ด๋ ์์ต๋๋ค.
Junit5์ ๋ฃฐ
@ExperimentalCoroutinesApi
class MainCoroutineExtension:
BeforEachCallback, AfterEachCallback {
lateinit var scheduler: TestCoroutineScheduler
private set
lateinit var dispatcher: TestDispatcher
private set
override fun beforEach(context: ExtensionContext?) {
scheduler = TestCoroutineScheduler()
dispatcher = StandardTestDispatcher(scheduler)
Dispatchers.setMain(dispatcher)
}
override fun afterEach(context: ExtensionContext?) {
Dispatchers.resetMain()
}
}
- MainCoroutineExtension์ ์ฌ์ฉ๋ฒ์ Junit4์ MainCoroutineRule๊ณผ ๊ฑฐ์ ๋์ผํ๋ฉฐ, ์ฐจ์ด์ ์ ๋ค๋ฅธ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ค๋ ๊ฒ์ ๋๋ค.
@JvmField
@RegisterExtension
var mainCoroutineExtension = MainCoroutineExtension()