Share
Embed
suspend関数のテストの疑問調べてみた
DroidKaigi.collect { #31@Nagoya }
https://droidkaigi.connpass.com/event/407026/
usuiat
October 23, 2026
Transcript
22 slides
-
About Me Atsushi Usui 臼井 篤志 Android Engineer Cybozu, Inc.
サイボウズ株式会社 Book『詳解 Jetpack Compose』 https://gihyo.jp/book/2024/978-4-297-14488-3 @usuiat Library “Zoomable” https://github.com/usuiat/Zoomable -
なんとなく苦手なヤツら Suspend関数のテスト Dispatcher Coroutine Context Scheduler • 最近はAIがセオリー通りのコードを書いてくれて、なぜその書き方なのか知らなく ても問題ないことも多い •
うっすらとした苦手意識をずっと持ちながら、「問題なさそう」でやり過ごす日々 理解したい!! -
suspend関数のテスト、runTestで書きますよね テスト対象 suspend fun addAfterDelay(a: Int, b: Int): Int {
delay(10.seconds) return a + b } テスト @Test fun test_with_runTest() = runTest { assertEquals(3, addAfterDelay(1, 2)) } -
runBlockingでは10秒かかるテストが、 runTestでは一瞬で完了する runTest @Test fun test_with_runTest() = runTest { assertEquals(3,
addAfterDelay(1, 2)) } runBlocking @Test fun test_with_runBlocking() = runBlocking { assertEquals(3, addAfterDelay(1, 2)) } -
delayのしくみ // Delay.kt(kotlinx.coroutines 1.11.0、抜粋) public suspend fun delay(timeMillis: Long) {
return suspendCancellableCoroutine { cont -> // Contextに入っているDelayの実装に任せる cont.context.delay .scheduleResumeAfterDelay(timeMillis, cont) } } delayはCoroutineContextに含 まれているDelayインター フェースの実装に処理を委ね ている • delay自身は中断するだけ • いつ再開するかはDelayの 実装次第 public interface Delay { fun scheduleResumeAfterDelay( timeMillis: Long, continuation: CancellableContinuation<Unit> ) } -
runTestはテスト用のDelayをCoroutineContextにセットする CoroutineContext(runTestがセット) delay(10.seconds) StandardTestDispatcher TestCoroutineScheduler Delayを実装 イベントと仮想時刻を管理 • runTestはCoroutineContextにStandardTestDispatcherとTestCoroutineSchedulerを セットする
• StandardTestDispatcher(の親クラスTestDispatcher)がDelayを実装している • runTest内でdelay(10.seconds)を呼ぶと、TestCoroutineSchedulerに「10秒後に再開 する」イベントが登録される -
TestCoroutineSchedulerは仮想時刻で動く // TestCoroutineScheduler.kt(抜粋) internal fun tryRunNextTaskUnless(...): Boolean { val event
= synchronized(lock) { val event = events.removeFirstOrNull() ?: return false currentTime = event.time // 仮想時刻を進めるだけ event } event.dispatcher.processEvent(event.marker) return true // ↑ すぐに再開 } TestCoroutineSchedulerの次のイベントを実行する処理 イベントの時刻まで実際 に待つのではなく、仮想 時刻を進めてすぐにイベ ントを実行する 仮想時刻の実態は、単純なカウ ンター → テストが高速に実 行できる -
公式のベストプラクティスでもDispatcherをDIしている // developer.android.com「コルーチンに関するベストプラクティス」より class NewsRepository( private val defaultDispatcher: CoroutineDispatcher =
Dispatchers.Default ) { suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ } } https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=ja -
DIできないと、delayがスキップされない class Sample { suspend fun add(a: Int, b: Int):
Int { return withContext(Dispatchers.Default) { delay(10.seconds) // ハードコード↑ a + b } } } @Test fun addTest() = runTest { val sample = Sample() assertEquals(3, sample.add(1, 2)) } このテストは通るが、10秒かかる -
DIしてTestDispatcherを渡すと一瞬 class Sample( private val dispatcher: CoroutineDispatcher = Dispatchers.Default )
{ suspend fun add(a: Int, b: Int): Int { return withContext(dispatcher) { delay(10.seconds) a + b } } } @Test fun addTest() = runTest { // スケジューラーを共有して同じ仮想時間で管理 val dispatcher = StandardTestDispatcher(testScheduler) val sample = Sample(dispatcher) assertEquals(3, sample.add(1, 2)) } withContextで使うDispatcherを外部 から渡せるようにし、テストでは StandardTestDispatcherを渡せば、 内側もdelayがスキップされる