KotlinのFlow用テストライブラリ「turbine」を使ってみる
日付:2023/03/07
URL: https://github.com/cashapp/turbine
調査者:Ryo Okizuka.icon
動機
とあるインターンでテストコードを書いていた時の事。「turbine」というライブラリを使って書き上げてプルリクを作成した。
するとレビュアーから「このライブラリなにこれすごいな」というコメントを頂いた。
結構気に入っているライブラリなのだが知られてなくて悲しかったため記事に書くことにした。
Flowのテストの難しさ
ここで、以下のtestTarget関数のテストを書いてみる。
code:kotlin
suspend fun testTarget(): Flow<String> = flow {
emit("First")
delay(1000L)
emit("Second")
delay(1000L)
emit("Third")
}
fun main() = runBlocking {
testTarget().collect { println(it) }
}
一秒ごとに文字列を返す関数で、出力結果は以下のようになる。
code:sh
First
Second
Third
Process finished with exit code 0
テストで確認したい項目は以下の通り
- First、Second、Thridの順番に出力される
- これ以上の値は出力されない
例えば以下のようなテストコードが考えられる。(もっとスマートに書く方法はあるかもしれない)
code:kotlin
@Test
fun testTargetのテスト() = runTest {
testTarget().collectIndexed { index, value ->
if (index == 0) assertEquals("First", value)
if (index == 1) assertEquals("Second", value)
if (index == 2) assertEquals("Third", value)
if (index >= 3) throw IndexOutOfBoundsException()
}
}
比較的スッキリと記述できているがいくつか気になる点がある。
1.いちいちindexの値を見るコードを書かないといけない
これにより、ただコードを書く行が増えるだけではなく、変更に弱くなっている。例えば以下のように対象コードを修正したとする。 
code:kotlin
suspend fun testTarget(): Flow<String> = flow {
emit("First")
delay(1000L)
emit("Second")
delay(1000L)
emit("Hoge")
delay(1000L)
emit("Third")
}
FirstとSecondの間にHogeを挟んだだけだが、Hoge以降のindexが全て狂うため全部書き直す必要が出てくる。
2.throwする内容が統一されない可能性がある
今回用意したテストコードでは、Flowで流す値が多い場合にIndexOutOfBountdExceptionが投げるように書かれている。しかし、多くのテストコードを書くうちにここが不明瞭になる可能性がある。複数人で開発しているときはなおさらである。
以上の問題点が考えられる。
これ以外にも、Flowの関数が複雑になる(例えばMutableStateFlowを用いる等)ごとにどんどん書きづらくなる。 (他に何かあるだろうか)
テストが読みづらいコードであるとメンテナンス性が著しく低下するため、もう少し書きやすくしたいところである。
turbineを利用
ここで「turbine」の出番である。turbineはFlow用テストライブラリで、非常にシンプルに記述することができる。
先ほどまでのコードはturbineを使うことで以下のように書くことができる。
code:kotlin
@Test
fun testTargetのテスト() = runTest {
testTarget().test {
assertEquals("First", awaitItem())
assertEquals("Second", awaitItem())
assertEquals("Third", awaitItem())
awaitComplete()
}
}
test {}、awaitItem()、awaitComplete()がturbineライブラリで提供されている関数である。
これで上記の問題を解決できている。また可読性も向上している。
turbineの導入もGradleに書き込めば済むので簡単である。
メソッド紹介(一部)
test()
turbineを適用するFlowを指定するための関数。
timeout等も指定可能である。
code:kotlin
targetFlow.test {
// turbineのコードをここに記述
}
awaitItem()
値が流れてくるのを待ち、流れてきたらその値を返す。
複数回呼び出した場合、流れてきた値順に値がかえされることになる。
code:kotlin
targetFlow.test {
val first = awaitItem() // 値が流れてくるまでブロッキング。流れてきたらその値を返す
val second = awaitItem() // 次の値が流れてくるまでブロッキング。
}
awaitComplete()
Flowが閉じたことを確認する関数。
新しく値が流れてきた場合TurbineAssertionErrorを投げる。
code:kotlin
targetFlow.test {
val value = awaitItem() // 値が流れてくるまでブロッキング。流れてきたらその値を返す
awaitComplete() // 新しい値が流れてきたらエラーが投げられる。
}
awaitError()
Flowでエラーが投げられたことを確認し、返り値として返す。
値が流れてきたりFlowが終了した場合、TurbineAssertionErrorを投げる。
code:kotlin
targetFlow.test {
val value = awaitItem() // 値が流れてくるまでブロッキング。流れてきたらその値を返す
val throwable = awaitError() // 新しい値が流れてきたらエラーが投げられる。投げられたエラーを返り値として返す
}
基本的にはこれらの関数で成り立つ。
詳しくは公式のGithubや資料を参照するとよさげ。
https://github.com/cashapp/turbine
https://cashapp.github.io/turbine/docs/0.x/turbine/app.cash.turbine/index.html
#Kotlin #research