FlowのテストライブラリTurbineのStandalone Turbinesについて



Flowのテストを書くためにcashapp製の[Turbine](https://github.com/cashapp/turbine)を使うことが多いのではないかと思います。私もバリバリ使っていて、Flowのテストを簡単に書くことができます。

本記事はTurbineのStandalone Turbinesという機能について、まとめてみようと思います。

この機能は結構良い機能なんですが、あまり記事として取り上げられているのを見かけないので、
活用方法含めて示していけたらなと思います。

なおTurbineの基本については軽く触れますが、 そちらに関してはいくつか記事もありますので、
そうした記事と公式の[README](https://github.com/cashapp/turbine)を見ていただければと思います。

また本記事のサンプルコードは[こちら](https://github.com/hiroaki404/PublicPlayground/blob/main/test/turbine/src/test/java/com/example/turbine/TurbineSimpleTest.kt)で確認できます。

## 簡単なTurbineの概要

ライブラリなくてもFlowをテストすることはできるのですが、Turbineを使うと書きやすくなります。
本題ではないので簡単な紹介ですが、以下はTurbineの恩恵が大きいSharedFlowのテストの例です。
(もちろん普通のFlowのテストでも有益ですが、今回は省略します。)


SharedFlowは収集を開始する必要がありますが、runTest内で収集をはじめると、SharedFlowは終わらないFlowなのでそこでテストが止まってしまします。
そのためbackgroundScopeで収集を開始する必要があります。
これは毎回面倒で、ボイラーコードが入るので可読性も落ちます。

```kotlin
val values = mutableListOf<Int>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
    sharedFlow.toList(values)
}

sharedFlow.emit(1)
values[0] shouldBe 1
sharedFlow.emit(2)
values[1] shouldBe 2
```

Turbineは`test` extensionのAPIを供給し、テストを書きやすくしてくれます。

```kotlin
sharedFlow.test {
    sharedFlow.emit(1)
    awaitItem() shouldBe 1
    sharedFlow.emit(2)
    awaitItem() shouldBe 2
}
```

TurbineはAndroid Developers公式の[Testing Kotlin flows on Android](https://developer.android.com/kotlin/flow/test#turbine)でも紹介されています。


## Standalone Turbinesとは

本記事の本題です。公式の説明は[こちら](https://github.com/cashapp/turbine?tab=readme-ov-file#standalone-turbines)です。

Turbineは中で何をやっているかと言うと、内部で`ReceiveTurbine`というものを作っています。
上記コード例では`.test { }`の呼び出しの内部で、ReceiveTurbineを作成しています。
`ReceiveTurbine`はTurbineライブラリで定義されるインタフェースで、flowの出力イベントを受け取る`awaitItem()`とかが定義されています。

Standalone Turbinesは検証用の`.test {}`呼び出しとは違って、Flowの外のテストコードとコミュニケーションを行うためにReceiveTurbineを作成します。
この説明では分かりづらいかもしれませんが、用途を考えたほうが分かりやすいです。
公式の[README](https://github.com/cashapp/turbine?tab=readme-ov-file#standalone-turbines)では、Fakeを使うときに、Fakeの期待するメソッドが呼び出されたか、という検証を行っています。

```kotlin
interface Logger {
    fun log(message: String)
}
class FakeLogger : Logger {
    val message = Turbine<String>()

    override fun log(message: String) {
        this.message.add(message)
    }
}

class GreetingRepository(val logger: Logger) {
    private val _greeting = "hello"
    fun greeting() = flow {
        val greeting = _greeting
        logger.log(greeting)
        emit(greeting)
    }
}

val fakeLogger = FakeLogger()
val greetingRepository = GreetingRepository(fakeLogger)
greetingRepository.greeting().test {
    awaitItem() shouldBe "hello"
    fakeLogger.message.awaitItem() shouldBe "hello" // 検証
    awaitComplete()
}
```

このようにメソッド呼び出しの検証のテストがいい感じに書けます。

## suspend関数でStandalone Turbinesを使う

上記が公式のREADMEで紹介されているStandalone Turbinesの使い方ですが、suspend関数の待ち合わせに使うことができます。
suspend関数をテストから簡単に制御できるので、おすすめです。

Fakeの中でStandalone Turbineを作ることは先程と同じですが、suspend関数で`awaitItem()`を呼び出します。
こうすることで、suspend関数の中断を制御できます。テストコードからStandalone Turbineにアクセスして`add()`を呼び出すことで、suspend関数の続きの処理を実行させます。

```kotlin
interface GreetingApi {
    suspend fun getGreeting(): String
}
class FakeGreetingApi : GreetingApi {
    val greeting = Turbine<String>()

    override suspend fun getGreeting(): String {
        return greeting.awaitItem()
    }
}

class GreetingRepository(val api: GreetingApi) {
    fun greeting() = flow {
        val greeting = api.getGreeting()
        emit(greeting)
    }
}

val fakeGreetingApi = FakeGreetingApi()
val greetingRepository = GreetingRepository(fakeGreetingApi)
greetingRepository.greeting().test {
    expectNoEvents() // この時点ではgetGreetingが中断中なのでイベントが起きていない
    fakeGreetingApi.greeting.add("hello")
    awaitItem() shouldBe "hello" // getGreetingが実行されている
    awaitComplete()
}
```

メリットとして、suspend関数が中断している状況、たとえばネットワークからレスポンス待ちの状態をテストできます。
suspend関数が複数絡んできたりしたときにも役立ち、どの順序で処理を行うか、といったものを制御できますね。

注意点として、上記コードは説明のためのコードとなっているので、実運用ではもう少しFakeの設計を工夫する余地があります。
というのもテストコードが内部実装に結びついてしまっていて、壊れやすくなっています。テストコードからの使いやすさも改善の余地があります。
実際に運用するときは、そのあたり考慮する必要があるでしょう。

## 備考:その他便利なAPI

Turbineにはいろいろ便利なAPIがあります。それぞれ使い所があります。
普段から使っていてノウハウが溜まっているので、そのあたり今後機会がありましたら共有します。
また前項で割愛したFakeの良い設計方法なども、今後紹介できればと思います。

## 備考:Qita CLIについて

この記事でqitacliを使ってみました。

[increments/qiita\-cli: Qiita CLI](https://github.com/increments/qiita-cli)とは、手元の環境で記事の執筆・プレビュー・投稿ができるツールです。

自分は普段Neovimで記事を書いていて、プラグインのマークダウンのプレビューを使っていたのですが、
Qitaに投稿すると見た目が少し変わるため、あとで修正することがよくありました。

今回使ってみて、記事を書きながら簡単にプレビューできました。
使い勝手もよいので、今後も使っていこうかと思います。