Flowのテストを書くためにcashapp製のTurbineを使うことが多いのではないかと思います。私もバリバリ使っていて、Flowのテストを簡単に書くことができます。
本記事はTurbineのStandalone Turbinesという機能について、まとめてみようと思います。
この機能は結構良い機能なんですが、あまり記事として取り上げられているのを見かけないので、
活用方法含めて示していけたらなと思います。
なおTurbineの基本については軽く触れますが、 そちらに関してはいくつか記事もありますので、
そうした記事と公式のREADMEを見ていただければと思います。
また本記事のサンプルコードはこちらで確認できます。
簡単なTurbineの概要
ライブラリなくてもFlowをテストすることはできるのですが、Turbineを使うと書きやすくなります。
本題ではないので簡単な紹介ですが、以下はTurbineの恩恵が大きいSharedFlowのテストの例です。
(もちろん普通のFlowのテストでも有益ですが、今回は省略します。)
SharedFlowは収集を開始する必要がありますが、runTest内で収集をはじめると、SharedFlowは終わらないFlowなのでそこでテストが止まってしまします。
そのためbackgroundScopeで収集を開始する必要があります。
これは毎回面倒で、ボイラーコードが入るので可読性も落ちます。
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を供給し、テストを書きやすくしてくれます。
sharedFlow.test {
sharedFlow.emit(1)
awaitItem() shouldBe 1
sharedFlow.emit(2)
awaitItem() shouldBe 2
}
TurbineはAndroid Developers公式のTesting Kotlin flows on Androidでも紹介されています。
Standalone Turbinesとは
本記事の本題です。公式の説明はこちらです。
Turbineは中で何をやっているかと言うと、内部でReceiveTurbine
というものを作っています。
上記コード例では.test { }
の呼び出しの内部で、ReceiveTurbineを作成しています。
ReceiveTurbine
はTurbineライブラリで定義されるインタフェースで、flowの出力イベントを受け取るawaitItem()
とかが定義されています。
Standalone Turbinesは検証用の.test {}
呼び出しとは違って、Flowの外のテストコードとコミュニケーションを行うためにReceiveTurbineを作成します。
この説明では分かりづらいかもしれませんが、用途を考えたほうが分かりやすいです。
公式のREADMEでは、Fakeを使うときに、Fakeの期待するメソッドが呼び出されたか、という検証を行っています。
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関数の続きの処理を実行させます。
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とは、手元の環境で記事の執筆・プレビュー・投稿ができるツールです。
自分は普段Neovimで記事を書いていて、プラグインのマークダウンのプレビューを使っていたのですが、
Qitaに投稿すると見た目が少し変わるため、あとで修正することがよくありました。
今回使ってみて、記事を書きながら簡単にプレビューできました。
使い勝手もよいので、今後も使っていこうかと思います。