トップ画像
KotlinのFlow用テストライブラリ「turbine」を使ってみる

執筆者: オキリョウ

最終更新: 2023/09/13

[2023/9/13更新] 記事の内容を修正、turbineのバージョンを1.0.0に修正

先日、インターン先で「turbine」を使ってテストコードを記述した。
するとレビュアーから「このライブラリなにこれすごいな」というコメント。意外に知られていないらしい。

個人的に気に入っているライブラリなので布教用に記事を書くことにした。

turbineとは

KotlinのFlowから流れてくる値をテストするためのライブラリ。
Kotlin/JVMはもちろん、Kotlin/JS、Kotlin/Nativeでも動作する。

Githubはこちら

記述例

class FooViewModel {
    val text = MutableStateFlow("")
  
    fun updateText(value: String) {
        text.emit(value)
    }
}


上のプログラムのテストを記述したい。
素のKotlinで記述すると以下の通り。

@Test
fun updateTextのテスト() = runTest {
    val viewModel = FooViewModel()
    val text = "text"

    viewModel.text.collectIndexed { index, value ->
        when {
            index == 0 -> {
                assertThat(value).isEqualsTo("")
                viewModel.updateText(text)
            }
            index == 1 -> assertThat(value).isEqualsTo(text)
            else -> throw IndexOutOfBoundsException()
        }
    }
    viewModel.updateText(text)
    delay(1000L) // 余計な値が流れてこないかを監視
}


turbineを利用すると以下の通り。

fun updateTextのテスト() = runTest {
    val viewModel = FooViewModel()
    val text = "text"

    viewModel.text.test {
        assertThat(awaitItem()).isEqualsTo("")

        viewModel.updateText(text)
        assertThat(awaitItem()).isEqualsTo(text)

        ensureAllEventsConsumed()
    }
}


導入方法

mavenCentralにて配布されているため、依存関係を記述することで利用可能になる。
プロジェクトにGradleを採用している場合、build.gradle(.kts)に以下を追記する。

repositories {
  mavenCentral()
}
dependencies {
  testImplementation 'app.cash.turbine:turbine:1.0.0'
}


メソッド紹介(一部)

test()

turbineを適用するための拡張関数。
timeout等指定可能。

targetFlow.test {
    // turbineを使ったコードをここに記述
}


awaitItem()

値が流れてくるのを待ち、流れてきたらその値を返す。
複数回呼び出した場合、流れてきた値順に値がかえされることになる。

targetFlow.test {
    val first = awaitItem() // 値が流れてくるまでブロッキング。流れてきたらその値を返す
    val second = awaitItem() // 次の値が流れてくるまでブロッキング。
}


awaitComplete()

Flowが閉じたことを確認する関数。
新しく値が流れてきた場合TurbineAssertionErrorを投げる。

targetFlow.test {
    val value = awaitItem()
    awaitComplete() // 新しい値が流れてきたらエラーが投げられる。
}


ensureAllEventsConsumed()

Flowのキューに値が残ってないことを確認する関数。
キューに値が残っていた場合TurbineAssertionErrorを投げる。
StateFlowのように、基本閉じることのないFlowだとawaitComplete()が使えないためこちらを使う。

targetFlow.test {
    val value = awaitItem()
    ensureAllEventConsumed() // Flowのキューに値が残っていた場合エラーが投げられる。
}


awaitError()

Flowでエラーが投げられたことを確認し、返り値として返す。
値が流れてきたりFlowが終了した場合、TurbineAssertionErrorを投げる。

targetFlow.test {
    val value = awaitItem()
    val throwable = awaitError() // 新しい値が流れてきたらエラーが投げられる。投げられたエラーを返り値として返す
}


skipItems(count: Int)

引数で指定した数だけFlowに流れてきた値を無視する。
指定数分の値が流れてこなかったりエラー終了した場合、TurbineAssertionErrorを投げる。

targetFlow.test {
    skipItems(count = 2) // 2個分の値を無視する
    val third = awaitItem()
}


他にも複数のFlowを用いたテストを記述するための関数も存在する。
詳しくは公式のREADME.mdから。

終わりに

Flowのテストは地獄になりがちだが、turbineを用いることでシンプルに記述可能である。
ぜひ自身のプロジェクトでも採用してほしい。

取得に失敗しました

2020年度 入部

Twitter GitHub