0Pricing
Kotlin Academy · レッスン

TurbineによるStateFlowとSharedFlowのテスト

Turbineテストライブラリを使って、Flowベースの状態のユニットテストを記述します。

「TurbineによるStateFlowとSharedFlowのテスト」はCoddyKit上の無料Kotlin Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはKotlin Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Kotlin Academyコースには全4レッスンが含まれています。

なぜTurbineを使うのか

TurbineはKotlin Flow用のテストライブラリで、発行された値を簡単に検証できます。Turbineを使わない場合、Flowのテストにはタイムアウトを設定した手動のチャネル収集が必要です。

// Add to test dependencies:
// testImplementation("app.cash.turbine:turbine:1.0.0")
// testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun main() { println("Turbine + runTest = clean flow testing") }

test()を使った基本的なFlowのテスト

flow.test { }はFlowを収集し、awaitItem()、awaitComplete()、awaitError()を使って各発行値を検証できるようにします。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun testBasicFlow() = runTest {
    val flow = flowOf(1, 2, 3)
    flow.test {
        assertEquals(1, awaitItem())
        assertEquals(2, awaitItem())
        assertEquals(3, awaitItem())
        awaitComplete()
    }
}

StateFlowのテスト

StateFlowは、収集が開始されると常に現在の値を発行します。TurbineのawaitItem()で、その初期発行値を取得できます。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun testStateFlow() = runTest {
    val state = MutableStateFlow("loading")
    state.test {
        assertEquals("loading", awaitItem())  // initial value
        state.value = "success"
        assertEquals("success", awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}

SharedFlowイベントのテスト

replay=0のSharedFlowは、これから発生するイベントのみを発行します。先に収集を開始してから、イベントを発行してください。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun testSharedFlow() = runTest {
    val events = MutableSharedFlow<String>()
    events.test {
        events.emit("LoginSuccess")
        assertEquals("LoginSuccess", awaitItem())
        events.emit("Navigate")
        assertEquals("Navigate", awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}

仮想時間のためのrunTest

runTestは仮想時間を使用します。delay()の呼び出しによって仮想時間が即座に進むため、遅延を含むテストを高速に実行できます。

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
import app.cash.turbine.*
fun testDelayedFlow() = runTest {
    val flow = flow {
        delay(1000)  // virtual — runs instantly
        emit("done")
    }
    flow.test {
        assertEquals("done", awaitItem())
        awaitComplete()
    }
}

例外テストのためのawaitError

awaitError()は、Flowが例外で終了することを検証します。想定する例外型と組み合わせて使用してください。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun testFlowError() = runTest {
    val flow = flow {
        emit(1)
        throw RuntimeException("stream error")
    }
    flow.test {
        assertEquals(1, awaitItem())
        val error = awaitError()
        assertEquals("stream error", error.message)
    }
}

expectNoEventsとcancelAndIgnoreRemainingEvents

expectNoEvents()は、何も発行されていないことを検証します。cancelAndIgnoreRemainingEvents()は収集をキャンセルし、保留中の発行値を無視します。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun testNoEvents() = runTest {
    val flow = MutableSharedFlow<Int>()
    flow.test {
        expectNoEvents()  // nothing emitted yet
        flow.emit(1)
        assertEquals(1, awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}

Turbineを使ったViewModelのテスト

フェイクのリポジトリを注入し、状態の遷移を検証して、ViewModelのStateFlowをテストします。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
sealed class State { object Loading : State(); data class Data(val v: String) : State() }
class FakeRepo { suspend fun fetch() = "result" }
class VM(repo: FakeRepo, scope: kotlinx.coroutines.CoroutineScope) {
    private val _s = MutableStateFlow<State>(State.Loading)
    val state = _s.asStateFlow()
    init { scope.launch { _s.value = State.Data(repo.fetch()) } }
}
fun testVM() = runTest {
    val vm = VM(FakeRepo(), backgroundScope)
    vm.state.test {
        assertEquals(State.Loading, awaitItem())
        val data = awaitItem() as State.Data
        assertEquals("result", data.v)
        cancelAndIgnoreRemainingEvents()
    }
}

turbineScopeを使った複数のコレクター

turbineScope { }を使うと、複数のFlowを同時にテストできます。それぞれに専用のTurbineインスタンスが割り当てられます。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun testMultipleFlows() = runTest {
    val a = MutableStateFlow(1)
    val b = MutableStateFlow("x")
    turbineScope {
        val aTurbine = a.testIn(backgroundScope)
        val bTurbine = b.testIn(backgroundScope)
        assertEquals(1, aTurbine.awaitItem())
        assertEquals("x", bTurbine.awaitItem())
        a.value = 2
        assertEquals(2, aTurbine.awaitItem())
        aTurbine.cancelAndIgnoreRemainingEvents()
        bTurbine.cancelAndIgnoreRemainingEvents()
    }
}

Flowオペレーターのテスト

Turbineを使って、カスタムFlowオペレーターや変換の動作を単独でテストします。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun testOperators() = runTest {
    (1..5).asFlow()
        .filter { it % 2 == 0 }
        .map { it * 10 }
        .test {
            assertEquals(20, awaitItem())
            assertEquals(40, awaitItem())
            awaitComplete()
        }
}

擬似時間の手動進行を使ったテスト

delayを含むテストでは、testScheduler.advanceTimeBy()を使って仮想時間を正確に制御します。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun testTimedFlow() = runTest {
    val flow = flow {
        emit("start")
        delay(500)
        emit("end")
    }
    flow.test {
        assertEquals("start", awaitItem())
        testScheduler.advanceTimeBy(500)
        assertEquals("end", awaitItem())
        awaitComplete()
    }
}

よくある間違い

Hot Flowのテストでは、最後に必ずTurbineをキャンセルしてください。cancelAndIgnoreRemainingEvents()を忘れると、さらにイベントが発生するのを待ち続けてテストがハングします。

import app.cash.turbine.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
fun testGoodPractice() = runTest {
    val state = MutableStateFlow(0)
    state.test {
        awaitItem()  // initial 0
        state.value = 1
        awaitItem()  // 1
        // Always cancel hot flows:
        cancelAndIgnoreRemainingEvents()
    }
}

確認問題

次に発行された項目を収集し、先にFlowが完了またはエラーになった場合は例外をスローするTurbineの関数はどれですか。

まとめ

Turbineを使うと、awaitItem()、awaitComplete()、awaitError()によってFlowのテストが簡単になります。仮想時間にはrunTestを使用します。Hot Flowのテストは必ずcancelAndIgnoreRemainingEvents()でキャンセルしてください。

よくある質問

「TurbineによるStateFlowとSharedFlowのテスト」レッスンは無料ですか?

はい。「TurbineによるStateFlowとSharedFlowのテスト」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Kotlin Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Kotlin Academyコースには全4レッスンが含まれています。

「TurbineによるStateFlowとSharedFlowのテスト」で何を学びますか?

Turbineテストライブラリを使って、Flowベースの状態のユニットテストを記述します。 ブラウザで直接実行するハンズオンコードでKotlin Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Kotlin Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのKotlin Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「TurbineによるStateFlowとSharedFlowのテスト」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このKotlin Academyレッスンでコードを書いて実行できますか?

はい。すべてのKotlin Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. StateFlow:UI向けのHotな状態保持
  2. SharedFlow:イベントバスとワンショットイベント
  3. shareInとstateInでCold FlowをHotに変換する
  4. TurbineによるStateFlowとSharedFlowのテスト
← Kotlin Academyに戻る