執筆者: オキリョウ
最終更新: 2023/09/13
[2023/9/13更新] 記事の内容を修正、turbineのバージョンを1.0.0に修正
先日、インターン先で「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を用いることでシンプルに記述可能である。
ぜひ自身のプロジェクトでも採用してほしい。